diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 7b44fba6583..cecde581a9f 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -11,8 +11,8 @@ export function activate() { }); return { - renderCell: (_id: string, context: { element: HTMLElement, value: string }) => { - const rendered = markdownIt.render(context.value); + renderCell: (_id: string, context: { element: HTMLElement, value: string, text(): string }) => { + const rendered = markdownIt.render(context.value || context.text()); // todo@jrieken remove .value-usage context.element.innerHTML = rendered; // Insert styles into markdown preview shadow dom so that they are applied diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5d0471788b4..7384f11f3ea 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1238,34 +1238,97 @@ declare module 'vscode' { with(change: { start?: number, end?: number }): NotebookRange; } - // code specific mime types - // application/x.notebook.error-traceback - // application/x.notebook.stdout - // application/x.notebook.stderr - // application/x.notebook.stream + // todo@API document which mime types are supported out of the box and + // which are considered secure export class NotebookCellOutputItem { - // todo@API - // add factory functions for common mime types - // static textplain(value:string): NotebookCellOutputItem; - // static errortrace(value:any): 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`. + * @returns A new output item object. + */ + static text(value: string, mime?: 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` + * @returns A new output item object. */ - static error(err: Error): NotebookCellOutputItem; + static json(value: any, mime?: string): NotebookCellOutputItem; + /** + * Factory function to create a `NotebookCellOutputItem` from bytes. + * + * @param value An array of unsigned 8-bit integers. + * @param mime Optional MIME type, defaults to `application/octet-stream`. + * @returns A new output item object. + */ + //todo@API better names: bytes, raw, buffer? + static bytes(value: Uint8Array, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stdout` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stdout(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stderr` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stderr(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.error` mime type. + * + * @param value An error object. + * @returns A new output item object. + */ + static error(value: Error): NotebookCellOutputItem; + + /** + * The mime type which determines how the {@link NotebookCellOutputItem.value `value`}-property + * is interpreted. + * + * Notebooks have built-in support for certain mime-types, extensions can add support for new + * types and override existing types. + */ mime: string; - //todo@API string or Unit8Array? - // value: string | Uint8Array | unknown; - value: unknown; + /** + * The value of this output item. Must always be an array of unsigned 8-bit integers. + */ + //todo@API only Unit8Array + value: Uint8Array | unknown; metadata?: { [key: string]: any }; - constructor(mime: string, value: unknown, metadata?: { [key: string]: any }); + /** + * Create a new notbook cell output item. + * + * @param mime The mime type of the output item. + * @param value The value of the output item. + * @param metadata Optional metadata for this output item. + */ + constructor(mime: string, value: Uint8Array | unknown, metadata?: { [key: string]: any }); } // @jrieken transient diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index a406d945ee0..4fbe1ac207f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -17,6 +17,7 @@ import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainThreadNotebo import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Schemas } from 'vs/base/common/network'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { @@ -121,10 +122,16 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS async $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise { + const info = this._notebookService.getContributedNotebookType(options.viewType); + if (!info) { + throw new Error('UNKNOWN view type: ' + options.viewType); + } + // find a free URI for the untitled case + const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; let uri: URI; for (let counter = 1; ; counter++) { - let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}`, query: options.viewType }); + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: options.viewType }); if (!this._notebookService.getNotebookTextModel(candidate)) { uri = candidate; break; 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 72271993685..1dfecfab513 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3112,21 +3112,48 @@ export class NotebookCellOutputItem { return typeof (obj).mime === 'string'; } - static error(err: Error): NotebookCellOutputItem { - return new NotebookCellOutputItem( - 'application/x.notebook.error', - JSON.stringify({ name: err.name, message: err.message, stack: err.stack }) - ); + static error(err: Error | { name: string, message?: string, stack?: string }): NotebookCellOutputItem { + const obj = { + name: err.name, + message: err.message, + stack: err.stack + }; + return NotebookCellOutputItem.json(obj, 'application/vnd.code.notebook.error'); + } + + static stdout(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stdout'); + } + + static stderr(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/vnd.code.notebook.stderr'); + } + + static bytes(value: Uint8Array, mime: string = 'application/octet-stream'): NotebookCellOutputItem { + return new NotebookCellOutputItem(mime, value); + } + + static #encoder = new TextEncoder(); + + static text(value: string, mime: string = 'text/plain'): NotebookCellOutputItem { + const bytes = NotebookCellOutputItem.#encoder.encode(String(value)); + return new NotebookCellOutputItem(mime, bytes); + } + + static json(value: any, mime: string = 'application/json'): NotebookCellOutputItem { + const rawStr = JSON.stringify(value, undefined, '\t'); + return NotebookCellOutputItem.text(rawStr, mime); } 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/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts b/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts index 825b8bcabb2..d9838fa1869 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown.ts @@ -22,7 +22,7 @@ class NotebookViewportContribution extends Disposable implements INotebookEditor super(); this._warmupViewport = new RunOnceScheduler(() => this._warmupViewportNow(), 200); - + this._register(this._warmupViewport); this._register(this._notebookEditor.onDidScroll(() => { this._warmupViewport.schedule(); })); @@ -65,7 +65,7 @@ class NotebookViewportContribution extends Disposable implements INotebookEditor if (pickedMimeTypeRenderer.rendererId === BUILTIN_RENDERER_ID) { const renderer = this._notebookEditor.getOutputRenderer().getContribution(pickedMimeTypeRenderer.mimeType); if (renderer?.getType() === RenderOutputType.Html) { - const renderResult = renderer!.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType), DOM.$(''), this._notebookEditor.viewModel.uri) as IInsetRenderOutput; + const renderResult = renderer.render(output, output.model.outputs.filter(op => op.mime === pickedMimeTypeRenderer.mimeType), DOM.$(''), this._notebookEditor.viewModel.uri) as IInsetRenderOutput; this._notebookEditor.createOutput(viewCell, renderResult, 0); } return; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 3534ad26e6a..3099ad7e84e 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -216,6 +216,7 @@ .monaco-workbench .notebookOverlay .output { position: absolute; height: 0px; + font-size: var(--notebook-cell-output-font-size); user-select: text; -webkit-user-select: text; -ms-user-select: text; @@ -261,7 +262,7 @@ .monaco-workbench .notebookOverlay .output .cell-output-toolbar { position: absolute; top: 4px; - left: -30px; + left: -32px; width: 16px; height: 16px; cursor: pointer; @@ -347,6 +348,15 @@ display: none; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .notebook-folding-indicator.mouseover .codicon { + opacity: 0; + transition: opacity 0.5s; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .notebook-folding-indicator.mouseover:hover .codicon { + opacity: 1; +} + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { top: 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 90bdc027e00..a2ab4ed2b94 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -90,6 +90,7 @@ export interface IRenderMainframeOutput { type: RenderOutputType.Mainframe; supportAppend?: boolean; initHeight?: number; + disposable?: IDisposable; } export interface IRenderPlainHtmlOutput { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 6204eeaf2d6..c3de76e5f67 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -369,7 +369,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation) { + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); @@ -563,11 +563,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor collapsedIndicatorHeight, compactView, focusIndicator, - insertToolbarPosition + insertToolbarPosition, + fontSize } = this._notebookOptions.getLayoutConfiguration(); const styleSheets: string[] = []; + styleSheets.push(` + :root { + --notebook-cell-output-font-size: ${fontSize}px; + } + `); + if (compactView) { styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 14ce25b10ba..dac77a7f635 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; @@ -20,7 +20,7 @@ export class OutputRenderer extends Disposable { private readonly instantiationService: IInstantiationService ) { super(); - for (const desc of NotebookRegistry.getOutputTransformContributions()) { + for (const desc of OutputRendererRegistry.getOutputTransformContributions()) { try { const contribution = this.instantiationService.createInstance(desc.ctor, notebookEditor); contribution.getMimetypes().forEach(mimetype => { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts similarity index 66% rename from src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts rename to src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts index c382e015505..574aa94d015 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/rendererRegistry.ts @@ -9,20 +9,18 @@ import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbenc export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { - id: string; ctor: IOutputTransformCtor; } +export const OutputRendererRegistry = new class NotebookRegistryImpl { -export const NotebookRegistry = new class NotebookRegistryImpl { + readonly #outputTransforms: IOutputTransformDescription[] = []; - readonly outputTransforms: IOutputTransformDescription[] = []; - - registerOutputTransform(id: string, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { - this.outputTransforms.push({ id: id, ctor: ctor as IOutputTransformCtor }); + registerOutputTransform(ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { + this.#outputTransforms.push({ ctor: ctor as IOutputTransformCtor }); } getOutputTransformContributions(): IOutputTransformDescription[] { - return this.outputTransforms.slice(0); + return this.#outputTransforms.slice(0); } }; 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..60c40a9e613 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,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } 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'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,66 +20,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution as IOutputRendererContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; 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); -} - -class JSONRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } - - getMimetypes() { - return ['application/json']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - const str = items.map(item => JSON.stringify(item.value, null, '\t')).join(''); - - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - }, - automaticLayout: true, - }, { - isSimpleWidget: true - }); - - const mode = this.modeService.create('json'); - const resource = URI.parse(`notebook-output-${Date.now()}.json`); - const textModel = this.modelService.createModel(str, mode, resource, false); - editor.setModel(textModel); - - const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; - const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; - const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); - - editor.layout({ - height, - width - }); - - container.style.height = `${height + 8}px`; - - return { type: RenderOutputType.Mainframe, initHeight: height }; - } -} class JavaScriptRendererContrib extends Disposable implements IOutputRendererContribution { getType() { @@ -99,8 +44,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,8 +65,6 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut return ['text/x-javascript']; } - private readonly _cellDisposables = new Map(); - constructor( public notebookEditor: ICommonNotebookEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -132,50 +74,50 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut super(); } - override dispose(): void { - dispose(this._cellDisposables.values()); - this._cellDisposables.clear(); - super.dispose(); + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement): IRenderOutput { + const value = items.map(getStringValue).join(''); + return this._render(output, container, value, 'javascript'); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { + protected _render(output: ICellOutputViewModel, container: HTMLElement, value: string, modeId: string): IRenderOutput { + const disposable = new DisposableStore(); + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, getOutputSimpleEditorOptions(), { isSimpleWidget: true }); - let cellDisposables = this._cellDisposables.get(output.cellViewModel.handle); - cellDisposables?.dispose(); - cellDisposables = new DisposableStore(); - this._cellDisposables.set(output.cellViewModel.handle, cellDisposables); - - const str = items.map(item => getStringValue(item.value)).join(''); - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getOutputSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - } - }, { - isSimpleWidget: true - }); - - const mode = this.modeService.create('javascript'); - const resource = URI.parse(`notebook-output-${Date.now()}.js`); - const textModel = this.modelService.createModel(str, mode, resource, false); + const mode = this.modeService.create(modeId); + const textModel = this.modelService.createModel(value, mode, undefined, false); editor.setModel(textModel); const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); - editor.layout({ - height, - width - }); + editor.layout({ height, width }); - cellDisposables.add(editor); - cellDisposables.add(textModel); + disposable.add(editor); + disposable.add(textModel); container.style.height = `${height + 8}px`; - return { type: RenderOutputType.Mainframe }; + return { type: RenderOutputType.Mainframe, initHeight: height, disposable }; + } +} + +class JSONRendererContrib extends CodeRendererContrib { + + override getMimetypes() { + return ['application/json']; + } + + override 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(''); + + return this._render(output, container, str, 'jsonc'); } } @@ -185,7 +127,7 @@ class StreamRendererContrib extends Disposable implements IOutputRendererContrib } getMimetypes() { - return ['application/x.notebook.stdout', 'application/x.notebook.stream']; + return ['application/vnd.code.notebook.stdout', 'application/x.notebook.stdout', 'application/x.notebook.stream']; } constructor( @@ -202,7 +144,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); @@ -218,7 +160,7 @@ class StderrRendererContrib extends StreamRendererContrib { } override getMimetypes() { - return ['application/x.notebook.stderr']; + return ['application/vnd.code.notebook.stderr', 'application/x.notebook.stderr']; } override render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { @@ -228,6 +170,7 @@ class StderrRendererContrib extends StreamRendererContrib { } } +/** @deprecated */ class ErrorRendererContrib extends Disposable implements IOutputRendererContribution { getType() { return RenderOutputType.Mainframe; @@ -296,22 +239,18 @@ class JSErrorRendererContrib implements IOutputRendererContribution { } getMimetypes() { - return ['application/x.notebook.error']; + return ['application/vnd.code.notebook.error']; } render(_output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, _notebookUri: URI): IRenderOutput { const linkDetector = this._instantiationService.createInstance(LinkDetector); + type ErrorLike = Partial; + 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; + let err: ErrorLike; 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 +297,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); @@ -373,7 +312,7 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut } getMimetypes() { - return ['text/html']; + return ['text/html', 'image/svg+xml']; } constructor( @@ -383,34 +322,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; - return { - type: RenderOutputType.Html, - source: output, - htmlContent: str - }; - } -} - -class SVGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Html; - } - - getMimetypes() { - return ['image/svg+xml']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } - - 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, @@ -436,26 +348,26 @@ 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 disposable = new DisposableStore(); + for (let item of items) { + 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); container.appendChild(mdOutput); - }); - - return { type: RenderOutputType.Mainframe }; + disposable.add(mdRenderer); + } + return { type: RenderOutputType.Mainframe, disposable }; } } -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', 'image/gif']; } constructor( @@ -465,65 +377,59 @@ class PNGRendererContrib extends Disposable implements IOutputRendererContributi } render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - items.forEach(item => { + const disposable = new DisposableStore(); + + for (let item of items) { + + 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); + disposable.add(toDisposable(() => URL.revokeObjectURL(src))); + } else { + // OLD + const imagedata = item.value; + src = `data:${item.mime};base64,${imagedata}`; + } + const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/png;base64,${imagedata}`; + image.src = src; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); container.appendChild(display); - }); - return { type: RenderOutputType.Mainframe }; + } + return { type: RenderOutputType.Mainframe, disposable }; } } -class JPEGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } +OutputRendererRegistry.registerOutputTransform(JSONRendererContrib); +OutputRendererRegistry.registerOutputTransform(JavaScriptRendererContrib); +OutputRendererRegistry.registerOutputTransform(HTMLRendererContrib); +OutputRendererRegistry.registerOutputTransform(MdRendererContrib); +OutputRendererRegistry.registerOutputTransform(ImgRendererContrib); +OutputRendererRegistry.registerOutputTransform(PlainTextRendererContrib); +OutputRendererRegistry.registerOutputTransform(CodeRendererContrib); +OutputRendererRegistry.registerOutputTransform(JSErrorRendererContrib); +OutputRendererRegistry.registerOutputTransform(StreamRendererContrib); +OutputRendererRegistry.registerOutputTransform(StderrRendererContrib); +OutputRendererRegistry.registerOutputTransform(ErrorRendererContrib); - 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 }; +// --- utils --- +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); } } -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('plain', PlainTextRendererContrib); -NotebookRegistry.registerOutputTransform('code', CodeRendererContrib); -NotebookRegistry.registerOutputTransform('error-trace', ErrorRendererContrib); -NotebookRegistry.registerOutputTransform('jserror', JSErrorRendererContrib); -NotebookRegistry.registerOutputTransform('stream-text', StreamRendererContrib); -NotebookRegistry.registerOutputTransform('stderr', StderrRendererContrib); - -export function getOutputSimpleEditorOptions(): IEditorOptions { +function getOutputSimpleEditorOptions(): IEditorConstructionOptions { return { + dimension: { height: 0, width: 0 }, readOnly: true, wordWrap: 'on', overviewRulerLanes: 0, @@ -541,6 +447,7 @@ export function getOutputSimpleEditorOptions(): IEditorOptions { lineNumbers: 'off', scrollbar: { alwaysConsumeMouseWheel: false - } + }, + automaticLayout: true, }; } 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 c00f3c1f24a..7ff4851f4dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -26,7 +26,7 @@ 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, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { PreloadOptions, 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 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -192,7 +192,7 @@ export interface ICreationRequestMessage { type: 'html'; content: | { type: RenderOutputType.Html; htmlContent: string } - | { type: RenderOutputType.Extension; outputId: string; value: unknown; metadata: unknown; mimeType: string }; + | { type: RenderOutputType.Extension; outputId: string; value: unknown; valueBytes: Uint8Array, metadata: unknown; mimeType: string }; cellId: string; outputId: string; cellTop: number; @@ -339,6 +339,11 @@ export interface INotebookStylesMessage { }; } +export interface INotebookOptionsMessage { + type: 'notebookOptions'; + options: PreloadOptions; +} + export type FromWebviewMessage = | WebviewIntialized | IDimensionMessage @@ -387,7 +392,8 @@ export type ToWebviewMessage = | IUnhideMarkdownMessage | IUpdateSelectedMarkdownPreviews | IInitializeMarkdownMessage - | INotebookStylesMessage; + | INotebookStylesMessage + | INotebookOptionsMessage; export type AnyMessage = FromWebviewMessage | ToWebviewMessage; @@ -434,7 +440,7 @@ export class BackLayerWebView extends Disposable { public readonly notebookEditor: ICommonNotebookEditor, public readonly id: string, public readonly documentUri: URI, - public options: { + private options: { outputNodePadding: number, outputNodeLeftPadding: number, previewNodePadding: number, @@ -442,6 +448,8 @@ export class BackLayerWebView extends Disposable { leftMargin: number, rightMargin: number, runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number }, private readonly rendererMessaging: IScopedRendererMessaging | undefined, @IWebviewService readonly webviewService: IWebviewService, @@ -483,9 +491,12 @@ export class BackLayerWebView extends Disposable { leftMargin: number, rightMargin: number, runGutter: number, + dragAndDropEnabled: boolean, + fontSize: number }) { this.options = options; this._updateStyles(); + this._updateOptions(); } private _updateStyles() { @@ -495,6 +506,15 @@ export class BackLayerWebView extends Disposable { }); } + private _updateOptions() { + this._sendMessageToWebview({ + type: 'notebookOptions', + options: { + dragAndDropEnabled: this.options.dragAndDropEnabled + } + }); + } + private _generateStyles() { return { 'notebook-output-left-margin': `${this.options.leftMargin + this.options.runGutter}px`, @@ -505,6 +525,7 @@ export class BackLayerWebView extends Disposable { 'notebook-markdown-left-margin': `${this.options.markdownLeftMargin}px`, 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, + 'notebook-cell-output-font-size': `${this.options.fontSize}px` }; } @@ -652,11 +673,8 @@ export class BackLayerWebView extends Disposable { width: 100%; } - #container .output_container .output div { - overflow-x: auto; - } - #container > div > div > div.output { + font-size: var(--notebook-cell-output-font-size); width: var(--notebook-output-width); margin-left: var(--notebook-output-left-margin); padding-top: var(--notebook-output-node-padding); @@ -678,13 +696,15 @@ export class BackLayerWebView extends Disposable { box-sizing: border-box; white-space: nowrap; overflow: hidden; + white-space: initial; + color: var(--vscode-foreground); + } + + #container > div.preview.draggable { user-select: none; -webkit-user-select: none; -ms-user-select: none; - white-space: initial; cursor: grab; - - color: var(--vscode-foreground); } #container > div.preview.emptyMarkdownCell::before { @@ -768,7 +788,7 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
- + `; } @@ -1219,6 +1239,7 @@ var requirejs = (function() { this.markdownPreviewMapping.clear(); this.initializeMarkdown(mdCells); this._updateStyles(); + this._updateOptions(); } private shouldUpdateInset(cell: IGenericCellViewModel, output: ICellOutputViewModel, cellTop: number, outputOffset: number): boolean { @@ -1480,6 +1501,10 @@ var requirejs = (function() { const output = content.source.model; renderer = content.renderer; const outputDto = output.outputs.find(op => op.mime === content.mimeType); + + // TODO@notebook - the message can contain "bytes" and those are transferable + // which improves IPC performance and therefore should be used. However, it does + // means that the bytes cannot be used here anymore message = { ...messageBase, outputId: output.outputId, @@ -1489,6 +1514,7 @@ var requirejs = (function() { outputId: output.outputId, mimeType: content.mimeType, value: outputDto?.value, + valueBytes: new Uint8Array(outputDto?.valueBytes ?? []), metadata: outputDto?.metadata, }, }; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index 03eea9d8ec5..ce804608e0d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -301,6 +301,10 @@ export class CellDragAndDropController extends Disposable { dragHandle.setAttribute('draggable', 'true'); templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled) { + return; + } + // Note, templateData may have a different element rendered into it by now container.classList.remove(DRAGGING_CLASS); this.dragCleanup(); @@ -311,6 +315,10 @@ export class CellDragAndDropController extends Disposable { return; } + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled) { + return; + } + this.currentDraggedCell = templateData.currentRenderedCell!; this.currentDraggedCell.dragging = true; @@ -324,11 +332,19 @@ export class CellDragAndDropController extends Disposable { } public startExplicitDrag(cell: ICellViewModel, _dragOffsetY: number) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled) { + return; + } + this.currentDraggedCell = cell; this.setInsertIndicatorVisibility(true); } public explicitDrag(cell: ICellViewModel, dragOffsetY: number) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled) { + return; + } + const target = this.list.elementAt(dragOffsetY); if (target && target !== cell) { const cellTop = this.list.getAbsoluteTopOfElement(target); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index e7362a45304..1b0270c2b6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -169,6 +169,13 @@ export class StatefulMarkdownCell extends Disposable { this.foldingState = viewCell.foldingState; this.setFoldingIndicator(); + this.updateFoldingIconShowClass(); + + this._register(this.notebookEditor.notebookOptions.onDidChangeOptions(e => { + if (e.showFoldingControls) { + this.updateFoldingIconShowClass(); + } + })); this._register(viewCell.onDidChangeState((e) => { if (!e.foldingStateChanged) { @@ -241,6 +248,11 @@ export class StatefulMarkdownCell extends Disposable { super.dispose(); } + private updateFoldingIconShowClass() { + const showFoldingIcon = this.notebookEditor.notebookOptions.getLayoutConfiguration().showFoldingControls; + this.templateData.foldingIndicator.classList.add(showFoldingIcon); + } + private viewUpdate(): void { if (this.viewCell.metadata.inputCollapsed) { this.viewUpdateCollapsed(); 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 40f70ec98f8..f097895be8b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -39,9 +39,15 @@ interface PreloadStyles { readonly outputNodeLeftPadding: number; } +export interface PreloadOptions { + dragAndDropEnabled: boolean; +} + declare function __import(path: string): Promise; -async function webviewPreloads(style: PreloadStyles, rendererData: readonly RendererMetadata[]) { +async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, rendererData: readonly RendererMetadata[]) { + let currentOptions = options; + const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); delete (globalThis as any).acquireVsCodeApi; @@ -459,6 +465,11 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend mime: string; value: unknown; metadata: unknown; + + text(): string; + json(): any; + bytes(): Uint8Array + blob(): Blob; } interface IDestroyCellInfo { @@ -632,6 +643,19 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend mime: content.mimeType, value: content.value, metadata: content.metadata, + bytes() { + return content.valueBytes; + }, + text() { + return new TextDecoder().decode(content.valueBytes) + || String(content.value); //todo@jrieken remove this once `value` is gone! + }, + json() { + return JSON.parse(this.text()); + }, + blob() { + return new Blob([content.valueBytes], { type: content.mimeType }); + } }); } catch (e) { showPreloadErrors(outputNode, e); @@ -791,6 +815,16 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend for (const variable of Object.keys(event.data.styles)) { documentStyle.setProperty(`--${variable}`, event.data.styles[variable]); } + break; + case 'notebookOptions': + currentOptions = event.data.options; + + // Update markdown previews + for (const markdownContainer of document.querySelectorAll('.preview')) { + setMarkdownContainerDraggable(markdownContainer, currentOptions.dragAndDropEnabled); + } + + break; } }); @@ -1002,6 +1036,10 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend mime: 'text/markdown', metadata: undefined, outputId: undefined, + text() { return content; }, + json() { return undefined; }, + bytes() { return new Uint8Array(); }, + blob() { return new Blob(); }, }); } }(); @@ -1011,6 +1049,16 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend type: 'initialized' }); + function setMarkdownContainerDraggable(element: Element, isDraggable: boolean) { + if (isDraggable) { + element.classList.add('draggable'); + element.setAttribute('draggable', 'true'); + } else { + element.classList.remove('draggable'); + element.removeAttribute('draggable'); + } + } + async function createMarkdownPreview(cellId: string, content: string, top: number): Promise { const container = document.getElementById('container')!; const cellContainer = document.createElement('div'); @@ -1058,7 +1106,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend postNotebookMessage('mouseLeaveMarkdownPreview', { cellId }); }); - cellContainer.setAttribute('draggable', 'true'); + setMarkdownContainerDraggable(cellContainer, currentOptions.dragAndDropEnabled); cellContainer.addEventListener('dragstart', e => { markdownPreviewDragManager.startDrag(e, cellId); @@ -1190,6 +1238,10 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return; } + if (!currentOptions.dragAndDropEnabled) { + return; + } + this.currentDrag = { cellId, clientY: e.clientY }; (e.target as HTMLElement).classList.add('dragging'); @@ -1240,13 +1292,14 @@ export interface RendererMetadata { readonly messaging: boolean; } -export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { +export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[]) { // TS will try compiling `import()` in webviePreloads, so use an helper function instead // of using `import(...)` directly return ` const __import = (x) => import(x); (${webviewPreloads})( JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(styleValues))}")), + JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(options))}")), JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(renderers))}")) )\n//# sourceURL=notebookWebviewPreloads.js\n`; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts index aa05e729203..64cf3c76b87 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts @@ -7,10 +7,9 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICellOutput, IOutputDto, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -let _handle = 0; export class NotebookCellOutputTextModel extends Disposable implements ICellOutput { - handle = _handle++; - private _onDidChangeData = new Emitter(); + + private _onDidChangeData = this._register(new Emitter()); onDidChangeData = this._onDidChangeData.event; get outputs() { @@ -38,14 +37,6 @@ export class NotebookCellOutputTextModel extends Disposable implements ICellOutp appendData(items: IOutputItemDto[]) { this._rawOutput.outputs.push(...items); - // for (const property in data) { - // if ((property === 'text/plain' || property === 'application/x.notebook.stream') && this._data[property] !== undefined) { - // const original = (isArray(this._data[property]) ? this._data[property] : [this._data[property]]) as string[]; - // const more = (isArray(data[property]) ? data[property] : [data[property]]) as string[]; - // this._data[property] = [...original, ...more]; - // } - // } - this._onDidChangeData.fire(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 1a121b5d1c4..b963316e2f2 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { Range } from 'vs/editor/common/core/range'; @@ -228,6 +228,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } override dispose() { + dispose(this._outputs); // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a CellTextModel reference const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 560db334bc9..7c748333af8 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; } @@ -577,20 +578,25 @@ type MimeTypeInfo = { }; const _mimeTypeInfo = new Map([ + ['application/javascript', { supportedByCore: true }], + ['image/png', { alwaysSecure: true, supportedByCore: true }], + ['image/jpeg', { alwaysSecure: true, supportedByCore: true }], + ['image/git', { alwaysSecure: true, supportedByCore: true }], + ['image/svg+xml', { supportedByCore: true }], ['application/json', { alwaysSecure: true, supportedByCore: true }], ['text/markdown', { alwaysSecure: true, supportedByCore: true }], - ['image/png', { alwaysSecure: true, supportedByCore: true }], ['text/plain', { alwaysSecure: true, supportedByCore: true }], - ['application/javascript', { supportedByCore: true }], ['text/html', { supportedByCore: true }], - ['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/vnd.code.notebook.error', { alwaysSecure: true, supportedByCore: true }], + ['application/vnd.code.notebook.stdout', { alwaysSecure: true, supportedByCore: true, mergeable: true }], + ['application/vnd.code.notebook.stderr', { alwaysSecure: true, supportedByCore: true, mergeable: true }], + // old, todo@jrieken remove these... ['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 { @@ -905,6 +911,8 @@ export const ExperimentalInsertToolbarPosition = 'notebook.experimental.insertTo export const ExperimentalGlobalToolbar = 'notebook.experimental.globalToolbar'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; export const ExperimentalConsolidatedOutputButton = 'notebook.experimental.consolidatedOutputButton'; +export const ExperimentalShowFoldingControls = 'notebook.experimental.showFoldingControls'; +export const ExperimentalDragAndDropEnabled = 'notebook.experimental.dragAndDropEnabled'; export const enum CellStatusbarAlignment { Left = 1, diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index b953078c681..0e877655b5b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -5,8 +5,8 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalConsolidatedOutputButton, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalConsolidatedOutputButton, ExperimentalDragAndDropEnabled, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ExperimentalShowFoldingControls, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const SCROLLABLE_ELEMENT_PADDING_TOP = 18; @@ -51,6 +51,9 @@ export interface NotebookLayoutConfiguration { insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; globalToolbar: boolean; consolidatedOutputButton: boolean; + showFoldingControls: 'always' | 'mouseover'; + dragAndDropEnabled: boolean; + fontSize: number; } interface NotebookOptionsChangeEvent { @@ -62,6 +65,10 @@ interface NotebookOptionsChangeEvent { focusIndicator?: boolean; insertToolbarPosition?: boolean; globalToolbar?: boolean; + showFoldingControls?: boolean; + consolidatedOutputButton?: boolean; + dragAndDropEnabled?: boolean; + fontSize?: boolean; } const defaultConfigConstants = { @@ -90,12 +97,15 @@ export class NotebookOptions { const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); const globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; const consolidatedOutputButton = this.configurationService.getValue(ExperimentalConsolidatedOutputButton) ?? true; + const dragAndDropEnabled = this.configurationService.getValue(ExperimentalDragAndDropEnabled) ?? true; const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); - const focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; - const insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + const focusIndicator = this._computeFocusIndicatorOption(); + const insertToolbarPosition = this._computeInsertToolbarPositionOption(); + const showFoldingControls = this._computeShowFoldingControlsOption(); const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition); + const fontSize = this.configurationService.getValue('editor.fontSize'); this._disposables = []; this._layoutConfiguration = { @@ -116,80 +126,18 @@ export class NotebookOptions { showCellStatusBar, globalToolbar, consolidatedOutputButton, + dragAndDropEnabled, cellToolbarLocation, cellToolbarInteraction, compactView, focusIndicator, - insertToolbarPosition + insertToolbarPosition, + showFoldingControls, + fontSize }; this._disposables.push(this.configurationService.onDidChangeConfiguration(e => { - let cellStatusBarVisibility = e.affectsConfiguration(ShowCellStatusBarKey); - let cellToolbarLocation = e.affectsConfiguration(CellToolbarLocKey); - let cellToolbarInteraction = e.affectsConfiguration(CellToolbarVisibility); - let compactView = e.affectsConfiguration(ExperimentalCompactView); - let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); - let insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); - let globalToolbar = e.affectsConfiguration(ExperimentalGlobalToolbar); - let consolidatedOutputButton = e.affectsConfiguration(ExperimentalConsolidatedOutputButton); - - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition && !globalToolbar && !consolidatedOutputButton) { - return; - } - - let configuration = Object.assign({}, this._layoutConfiguration); - - if (cellStatusBarVisibility) { - configuration.showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); - } - - if (cellToolbarLocation) { - configuration.cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); - } - - if (cellToolbarInteraction) { - configuration.cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); - } - - if (focusIndicator) { - configuration.focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; - } - - if (compactView) { - const compactViewValue = this.configurationService.getValue('notebook.experimental.compactView'); - configuration = Object.assign(configuration, { - ...(compactViewValue ? compactConfigConstants : defaultConfigConstants), - }); - configuration.compactView = compactViewValue; - } - - if (insertToolbarPosition) { - configuration.insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; - const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(configuration.compactView, configuration.insertToolbarPosition); - configuration.bottomToolbarHeight = bottomToolbarHeight; - configuration.bottomToolbarGap = bottomToolbarGap; - } - - if (globalToolbar) { - configuration.globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; - } - - if (consolidatedOutputButton) { - configuration.consolidatedOutputButton = this.configurationService.getValue(ExperimentalConsolidatedOutputButton) ?? true; - } - - this._layoutConfiguration = configuration; - - // trigger event - this._onDidChangeOptions.fire({ - cellStatusBarVisibility: cellStatusBarVisibility, - cellToolbarLocation: cellToolbarLocation, - cellToolbarInteraction: cellToolbarInteraction, - compactView: compactView, - focusIndicator: focusIndicator, - insertToolbarPosition: insertToolbarPosition, - globalToolbar: globalToolbar - }); + this._updateConfiguration(e); })); this._disposables.push(EditorTopPaddingChangeEvent(() => { @@ -200,6 +148,117 @@ export class NotebookOptions { })); } + private _updateConfiguration(e: IConfigurationChangeEvent) { + const cellStatusBarVisibility = e.affectsConfiguration(ShowCellStatusBarKey); + const cellToolbarLocation = e.affectsConfiguration(CellToolbarLocKey); + const cellToolbarInteraction = e.affectsConfiguration(CellToolbarVisibility); + const compactView = e.affectsConfiguration(ExperimentalCompactView); + const focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); + const insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); + const globalToolbar = e.affectsConfiguration(ExperimentalGlobalToolbar); + const consolidatedOutputButton = e.affectsConfiguration(ExperimentalConsolidatedOutputButton); + const showFoldingControls = e.affectsConfiguration(ExperimentalShowFoldingControls); + const dragAndDropEnabled = e.affectsConfiguration(ExperimentalDragAndDropEnabled); + const fontSize = e.affectsConfiguration('editor.fontSize'); + + if ( + !cellStatusBarVisibility + && !cellToolbarLocation + && !cellToolbarInteraction + && !compactView + && !focusIndicator + && !insertToolbarPosition + && !globalToolbar + && !consolidatedOutputButton + && !showFoldingControls + && !dragAndDropEnabled + && !fontSize) { + return; + } + + let configuration = Object.assign({}, this._layoutConfiguration); + + if (cellStatusBarVisibility) { + configuration.showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); + } + + if (cellToolbarLocation) { + configuration.cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); + } + + if (cellToolbarInteraction) { + configuration.cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); + } + + if (focusIndicator) { + configuration.focusIndicator = this._computeFocusIndicatorOption(); + } + + if (compactView) { + const compactViewValue = this.configurationService.getValue('notebook.experimental.compactView'); + configuration = Object.assign(configuration, { + ...(compactViewValue ? compactConfigConstants : defaultConfigConstants), + }); + configuration.compactView = compactViewValue; + } + + if (insertToolbarPosition) { + configuration.insertToolbarPosition = this._computeInsertToolbarPositionOption(); + const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(configuration.compactView, configuration.insertToolbarPosition); + configuration.bottomToolbarHeight = bottomToolbarHeight; + configuration.bottomToolbarGap = bottomToolbarGap; + } + + if (globalToolbar) { + configuration.globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; + } + + if (consolidatedOutputButton) { + configuration.consolidatedOutputButton = this.configurationService.getValue(ExperimentalConsolidatedOutputButton) ?? true; + } + + if (showFoldingControls) { + configuration.showFoldingControls = this._computeShowFoldingControlsOption(); + } + + if (dragAndDropEnabled) { + configuration.dragAndDropEnabled = this.configurationService.getValue(ExperimentalDragAndDropEnabled) ?? true; + } + + if (fontSize) { + configuration.fontSize = this.configurationService.getValue('editor.fontSize'); + } + + this._layoutConfiguration = Object.freeze(configuration); + + // trigger event + this._onDidChangeOptions.fire({ + cellStatusBarVisibility, + cellToolbarLocation, + cellToolbarInteraction, + compactView, + focusIndicator, + insertToolbarPosition, + globalToolbar, + showFoldingControls, + consolidatedOutputButton, + dragAndDropEnabled, + fontSize: fontSize + }); + } + + private _computeInsertToolbarPositionOption() { + return this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + } + + private _computeShowFoldingControlsOption() { + return this.configurationService.getValue<'always' | 'mouseover'>(ExperimentalShowFoldingControls) ?? 'always'; + } + + private _computeFocusIndicatorOption() { + return this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; + } + private _computeBottomToolbarDimensions(compactView: boolean, insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'): { bottomToolbarGap: number, bottomToolbarHeight: number } { if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { return compactView ? { @@ -308,6 +367,8 @@ export class NotebookOptions { leftMargin: this._layoutConfiguration.codeCellLeftMargin, rightMargin: this._layoutConfiguration.cellRightMargin, runGutter: this._layoutConfiguration.cellRunGutter, + dragAndDropEnabled: this._layoutConfiguration.dragAndDropEnabled, + fontSize: this._layoutConfiguration.fontSize }; } @@ -319,7 +380,9 @@ export class NotebookOptions { markdownLeftMargin: 0, leftMargin: 0, rightMargin: 0, - runGutter: 0 + runGutter: 0, + dragAndDropEnabled: false, + fontSize: this._layoutConfiguration.fontSize }; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 7d94ed56c4a..fbdce0aebbd 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -104,4 +104,38 @@ export class NotebookProviderInfo { return false; } + + static possibleFileEnding(selectors: NotebookSelector[]): string | undefined { + for (let selector of selectors) { + const ending = NotebookProviderInfo._possibleFileEnding(selector); + if (ending) { + return ending; + } + } + return undefined; + } + + private static _possibleFileEnding(selector: NotebookSelector): string | undefined { + + const pattern = /^.*(\.[a-zA-Z0-9_-]+)$/; + + let candidate: string | undefined; + + if (typeof selector === 'string') { + candidate = selector; + } else if (glob.isRelativePattern(selector)) { + candidate = selector.pattern; + } else if (selector.include) { + return NotebookProviderInfo._possibleFileEnding(selector.include); + } + + if (candidate) { + const match = pattern.exec(candidate); + if (match) { + return match[1]; + } + } + + return undefined; + } } diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 66ca44e260a..353096359eb 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -682,4 +682,42 @@ suite('ExtHostTypes', function () { assert.deepStrictEqual(newCustom.mycustom, undefined); assert.deepStrictEqual(newCustom.anotherCustom, { display: 'hello2' }); }); + + test('NotebookCellOutputItem - factories', function () { + + // --- err + + let item = types.NotebookCellOutputItem.error(new Error()); + assert.strictEqual(item.mime, 'application/vnd.code.notebook.error'); + item = types.NotebookCellOutputItem.error({ name: 'Hello' }); + assert.strictEqual(item.mime, 'application/vnd.code.notebook.error'); + + // --- JSON + + item = types.NotebookCellOutputItem.json(1); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.value, new TextEncoder().encode(JSON.stringify(1))); + + item = types.NotebookCellOutputItem.json(1, 'foo'); + assert.strictEqual(item.mime, 'foo'); + assert.deepStrictEqual(item.value, new TextEncoder().encode(JSON.stringify(1))); + + item = types.NotebookCellOutputItem.json(true); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.value, new TextEncoder().encode(JSON.stringify(true))); + + item = types.NotebookCellOutputItem.json([true, 1, 'ddd']); + assert.strictEqual(item.mime, 'application/json'); + assert.deepStrictEqual(item.value, new TextEncoder().encode(JSON.stringify([true, 1, 'ddd'], undefined, '\t'))); + + // --- text + + item = types.NotebookCellOutputItem.text('Hęłlö'); + assert.strictEqual(item.mime, 'text/plain'); + assert.deepStrictEqual(item.value, new TextEncoder().encode('Hęłlö')); + + item = types.NotebookCellOutputItem.text('Hęłlö', 'foo/bar'); + assert.strictEqual(item.mime, 'foo/bar'); + assert.deepStrictEqual(item.value, new TextEncoder().encode('Hęłlö')); + }); });