From d00f117c6232cc1010249e7e53499977b6a98d66 Mon Sep 17 00:00:00 2001 From: rebornix Date: Fri, 21 Feb 2020 11:47:49 -0800 Subject: [PATCH] Mimetype picker first cut. --- extensions/notebook-renderers/package.json | 13 + .../notebook-renderers/src/extension.ts | 1 + extensions/notebook-test/package.json | 13 + extensions/notebook-test/src/extension.ts | 19 ++ src/vs/vscode.proposed.d.ts | 2 +- .../api/browser/mainThreadNotebook.ts | 4 +- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostNotebook.ts | 59 +++-- .../notebook/browser/extensionPoint.ts | 77 +++++- .../contrib/notebook/browser/notebook.css | 10 + .../notebook/browser/notebookService.ts | 62 ++++- .../notebook/browser/output/outputRenderer.ts | 4 +- .../output/transforms/richTransform.ts | 250 ++++++++++-------- .../browser/renderers/cellRenderer.ts | 2 +- .../notebook/browser/renderers/codeCell.ts | 112 +++++++- .../contrib/notebook/common/notebookCommon.ts | 9 +- .../notebook/common/notebookOutputRenderer.ts | 30 +++ 18 files changed, 508 insertions(+), 165 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts diff --git a/extensions/notebook-renderers/package.json b/extensions/notebook-renderers/package.json index 9b85015d4bd..14a74bb485b 100644 --- a/extensions/notebook-renderers/package.json +++ b/extensions/notebook-renderers/package.json @@ -22,6 +22,19 @@ "*" ], "contributes": { + "notebookOutputRenderer": [ + { + "viewType": "nteract", + "displayName": "nteract renderer for notebook", + "mimeTypes": [ + "text/latex", + "text/markdown", + "application/json", + "application/vnd.plotly.v1+json", + "application/vnd.vega.v5+json" + ] + } + ] }, "scripts": { "compile": "tsc -p ./", diff --git a/extensions/notebook-renderers/src/extension.ts b/extensions/notebook-renderers/src/extension.ts index 008d3ac930c..d2625e4fbe2 100644 --- a/extensions/notebook-renderers/src/extension.ts +++ b/extensions/notebook-renderers/src/extension.ts @@ -10,6 +10,7 @@ export function activate(context: vscode.ExtensionContext) { console.log(context.extensionPath); context.subscriptions.push(vscode.window.registerNotebookOutputRenderer( + 'nteract', { type: 'display_data', subTypes: [ diff --git a/extensions/notebook-test/package.json b/extensions/notebook-test/package.json index d2756f899d4..79b9bb8ef47 100644 --- a/extensions/notebook-test/package.json +++ b/extensions/notebook-test/package.json @@ -49,6 +49,19 @@ } ] } + ], + "notebookOutputRenderer": [ + { + "viewType": "kerneltest", + "displayName": "kernel test renderer for notebook", + "mimeTypes": [ + "text/latex", + "text/markdown", + "application/json", + "application/vnd.plotly.v1+json", + "application/vnd.vega.v5+json" + ] + } ] }, "scripts": { diff --git a/extensions/notebook-test/src/extension.ts b/extensions/notebook-test/src/extension.ts index 24295f9aa00..9044d32c17a 100644 --- a/extensions/notebook-test/src/extension.ts +++ b/extensions/notebook-test/src/extension.ts @@ -14,6 +14,25 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.registerNotebookProvider('jupyter', new NotebookProvider(context.extensionPath, true))); context.subscriptions.push(vscode.window.registerNotebookProvider('jupytertest', new NotebookProvider(context.extensionPath, false))); + context.subscriptions.push(vscode.window.registerNotebookOutputRenderer( + 'kerneltest', + { + type: 'display_data', + subTypes: [ + 'text/latex', + 'text/markdown', + 'application/json', + 'application/vnd.plotly.v1+json', + 'application/vnd.vega.v5+json' + ] + }, + { + render: () => { + return '

kernel test renderer

'; + } + } + )); + vscode.commands.registerCommand('notebook.saveToMarkdown', () => { if (vscode.window.activeNotebookDocument) { let document = vscode.window.activeNotebookDocument; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 71a203abd86..df4ac3e5f57 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1443,7 +1443,7 @@ declare module 'vscode' { provider: NotebookProvider ): Disposable; - export function registerNotebookOutputRenderer(outputSelector: NotebookOutputSelector, renderer: NotebookOutputRenderer): Disposable; + export function registerNotebookOutputRenderer(type: string, outputSelector: NotebookOutputSelector, renderer: NotebookOutputRenderer): Disposable; export let activeNotebookDocument: NotebookDocument | undefined; } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 9affac7817e..a2aac4fd668 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -228,8 +228,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo }); } - async $registerNotebookRenderer(extension: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise { - this._notebookService.registerNotebookRenderer(handle, extension, selectors, preloads.map(uri => URI.revive(uri))); + async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise { + this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri))); } async $unregisterNotebookRenderer(handle: number): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 455185f6f7d..dfd2ec920ea 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -583,8 +583,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { return extHostNotebook.registerNotebookProvider(extension, viewType, provider); }, - registerNotebookOutputRenderer: (outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { - return extHostNotebook.registerNotebookOutputRenderer(extension, outputFilter, renderer); + registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { + return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer); }, get activeNotebookDocument(): vscode.NotebookDocument | undefined { return extHostNotebook.activeNotebookDocument; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 98f6e41add2..326badb63c2 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -654,7 +654,7 @@ export type NotebookCellOutputsSplice = [ export interface MainThreadNotebookShape extends IDisposable { $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookRenderer(extension: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; + $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; $unregisterNotebookRenderer(handle: number): Promise; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 1c7e49fce60..570091742d9 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -239,20 +239,23 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { outputs = outputs.map(output => { let richestMimeType: string | undefined = undefined; - if (this.renderingHandler.outputDisplayOrder?.userOrder || this._parsedDisplayOrder.length > 0) { + if (this.renderingHandler.outputDisplayOrder?.defaultOrder || this.renderingHandler.outputDisplayOrder?.userOrder || this._parsedDisplayOrder.length > 0) { richestMimeType = this.findRichestMimeType(output); } + output.pickedMimeType = richestMimeType; + let transformedOutput: vscode.CellOutput | undefined = undefined; if (richestMimeType) { let handler = this.renderingHandler.findBestMatchedRenderer(richestMimeType); - if (handler) { - renderers.add(handler.handle); - transformedOutput = handler?.render(this, cell, output); + if (handler.length) { + renderers.add(handler[0].handle); + transformedOutput = handler[0].render(this, cell, output); - output = transformedOutput; - output.pickedMimeType = richestMimeType; + output.pickedRenderer = handler[0].handle; + // output.transformedOutput = transformedOutput; + output.transformedOutput = { richestMimeType: transformedOutput }; } } @@ -289,20 +292,23 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { outputs = outputs.map(output => { let richestMimeType: string | undefined = undefined; - if (this.renderingHandler.outputDisplayOrder?.userOrder || this._parsedDisplayOrder.length > 0) { + if (this.renderingHandler.outputDisplayOrder?.defaultOrder || this.renderingHandler.outputDisplayOrder?.userOrder || this._parsedDisplayOrder.length > 0) { richestMimeType = this.findRichestMimeType(output); } + (output).pickedMimeType = richestMimeType; let transformedOutput: vscode.CellOutput | undefined = undefined; if (richestMimeType) { let handler = this.renderingHandler.findBestMatchedRenderer(richestMimeType); - if (handler) { - renderers.add(handler.handle); - transformedOutput = handler?.render(this, cell, output); + if (handler.length) { + let pickedHandler = handler[0]; + renderers.add(pickedHandler.handle); + + transformedOutput = pickedHandler.render(this, cell, output); + (output).pickedRenderer = pickedHandler.handle; + (output).transformedOutput = { richestMimeType: transformedOutput }; - output = transformedOutput; - (output).pickedMimeType = richestMimeType; } } @@ -383,6 +389,16 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { order++; } + + let defaultOrder = coreDisplayOrder.defaultOrder; + + for (let i = 0; i < defaultOrder.length; i++) { + if (defaultOrder[i](mimeType)) { + return order; + } + + order++; + } } return order; @@ -486,8 +502,9 @@ export class ExtHostNotebookOutputRenderer { readonly handle = ExtHostNotebookOutputRenderer._handlePool++; constructor( - private filter: vscode.NotebookOutputSelector, - private renderer: vscode.NotebookOutputRenderer + public type: string, + public filter: vscode.NotebookOutputSelector, + public renderer: vscode.NotebookOutputRenderer ) { } @@ -517,7 +534,7 @@ export class ExtHostNotebookOutputRenderer { export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: ExtHostOutputDisplayOrder | undefined; - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer | undefined; + findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; } export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler { @@ -545,27 +562,29 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } registerNotebookOutputRenderer( + type: string, extension: IExtensionDescription, filter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer ): vscode.Disposable { - let extHostRenderer = new ExtHostNotebookOutputRenderer(filter, renderer); + let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); this._notebookOutputRenderers.set(extHostRenderer.handle, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, filter, extHostRenderer.handle, renderer.preloads || []); + this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, extHostRenderer.handle, renderer.preloads || []); return new VSCodeDisposable(() => { this._notebookOutputRenderers.delete(extHostRenderer.handle); this._proxy.$unregisterNotebookRenderer(extHostRenderer.handle); }); } - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer | undefined { + findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { + let matches: ExtHostNotebookOutputRenderer[] = []; for (let renderer of this._notebookOutputRenderers) { if (renderer[1].matches(mimeType)) { - return renderer[1]; + matches.push(renderer[1]); } } - return; + return matches; } registerNotebookProvider( diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index de2efbee51d..7dbcd235058 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,7 +6,6 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookProvider'; namespace NotebookEditorContribution { @@ -15,15 +14,28 @@ namespace NotebookEditorContribution { export const selector = 'selector'; } - interface INotebookEditorContribution { readonly [NotebookEditorContribution.viewType]: string; readonly [NotebookEditorContribution.displayName]: string; readonly [NotebookEditorContribution.selector]?: readonly NotebookSelector[]; } -const notebookContribution: IJSONSchema = { - description: nls.localize('contributes.notebook', 'Contributes notebook.'), +namespace NotebookRendererContribution { + export const viewType = 'viewType'; + export const displayName = 'displayName'; + export const mimeTypes = 'mimeTypes'; +} + +interface INotebookRendererContribution { + readonly [NotebookRendererContribution.viewType]: string; + readonly [NotebookRendererContribution.displayName]: string; + readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; +} + + + +const notebookProviderContribution: IJSONSchema = { + description: nls.localize('contributes.notebook.provider', 'Contributes notebook document provider.'), type: 'array', defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], items: { @@ -36,25 +48,25 @@ const notebookContribution: IJSONSchema = { properties: { [NotebookEditorContribution.viewType]: { type: 'string', - description: nls.localize('contributes.notebook.viewType', 'Unique identifier of the notebook.'), + description: nls.localize('contributes.notebook.provider.viewType', 'Unique identifier of the notebook.'), }, [NotebookEditorContribution.displayName]: { type: 'string', - description: nls.localize('contributes.notebook.displayName', 'Human readable name of the notebook.'), + description: nls.localize('contributes.notebook.provider.displayName', 'Human readable name of the notebook.'), }, [NotebookEditorContribution.selector]: { type: 'array', - description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), + description: nls.localize('contributes.notebook.provider.selector', 'Set of globs that the notebook is for.'), items: { type: 'object', properties: { filenamePattern: { type: 'string', - description: nls.localize('contributes.notebook.selector.filenamePattern', 'Glob that the notebook is enabled for.'), + description: nls.localize('contributes.notebook.provider.selector.filenamePattern', 'Glob that the notebook is enabled for.'), }, excludeFileNamePattern: { type: 'string', - description: nls.localize('contributes.notebook.selector.excludeFileNamePattern', 'Glob that the notebook is disabled for.') + description: nls.localize('contributes.notebook.selector.provider.excludeFileNamePattern', 'Glob that the notebook is disabled for.') } } } @@ -63,8 +75,45 @@ const notebookContribution: IJSONSchema = { } }; -export const notebookExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'notebookProvider', - deps: [languagesExtPoint], - jsonSchema: notebookContribution -}); +const notebookRendererContribution: IJSONSchema = { + description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), + type: 'array', + defaultSnippets: [{ body: [{ viewType: '', displayName: '', mimeTypes: [''] }] }], + items: { + type: 'object', + required: [ + NotebookRendererContribution.viewType, + NotebookRendererContribution.displayName, + NotebookRendererContribution.mimeTypes, + ], + properties: { + [NotebookRendererContribution.viewType]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), + }, + [NotebookRendererContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), + }, + [NotebookRendererContribution.mimeTypes]: { + type: 'array', + description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), + items: { + type: 'string' + } + } + } + } +}; + +export const notebookProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint( + { + extensionPoint: 'notebookProvider', + jsonSchema: notebookProviderContribution + }); + +export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( + { + extensionPoint: 'notebookOutputRenderer', + jsonSchema: notebookRendererContribution + }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css index ba2b6f5a4e6..32914a89530 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -37,6 +37,7 @@ padding-right: 8px; user-select: text; transform: translate3d(0px, 0px, 0px); + cursor: auto; } .monaco-workbench .part.editor > .content .notebook-editor .output p { @@ -45,6 +46,15 @@ margin: 0px; } +.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output { + position: absolute; + top: 4px; + left: -24px; + width: 16px; + height: 16px; + cursor: pointer; +} + .monaco-workbench .part.editor > .content .notebook-editor .output .error_message { color: red; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 1c6330d71b6..50fe556f28e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -6,12 +6,13 @@ import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { notebookExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter, Event } from 'vs/base/common/event'; import { INotebook, ICell, INotebookMimeTypeSelector } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -35,7 +36,7 @@ export interface INotebookService { onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }>; registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; unregisterNotebookProvider(viewType: string): void; - registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; + registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; unregisterNotebookRenderer(handle: number): void; getRendererPreloads(handle: number): URI[]; resolveNotebook(viewType: string, uri: URI): Promise; @@ -50,7 +51,7 @@ export interface INotebookService { updateActiveNotebookDocument(viewType: string, resource: URI): void; } -export class NotebookInfoStore { +export class NotebookProviderInfoStore { private readonly contributedEditors = new Map(); clear() { @@ -75,6 +76,31 @@ export class NotebookInfoStore { } } +export class NotebookOutputRendererInfoStore { + private readonly contributedRenderers = new Map(); + + clear() { + this.contributedRenderers.clear(); + } + + get(viewType: string): NotebookOutputRendererInfo | undefined { + return this.contributedRenderers.get(viewType); + } + + add(info: NotebookOutputRendererInfo): void { + if (this.contributedRenderers.has(info.id)) { + console.log(`Custom notebook output renderer with id '${info.id}' already registered`); + return; + } + this.contributedRenderers.set(info.id, info); + } + + getContributedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] { + return Array.from(this.contributedRenderers.values()).filter(customEditor => + customEditor.matches(mimeType)); + } +} + class ModelData implements IDisposable { private readonly _modelEventListeners = new DisposableStore(); @@ -94,8 +120,9 @@ class ModelData implements IDisposable { export class NotebookService extends Disposable implements INotebookService { _serviceBrand: undefined; private readonly _notebookProviders = new Map(); - private readonly _notebookRenderers = new Map(); - notebookProviderInfoStore: NotebookInfoStore = new NotebookInfoStore(); + private readonly _notebookRenderers = new Map(); + notebookProviderInfoStore: NotebookProviderInfoStore = new NotebookProviderInfoStore(); + notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); private readonly _models: { [modelId: string]: ModelData; }; private _onDidChangeActiveEditor = new Emitter<{ viewType: string, uri: URI }>(); onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }> = this._onDidChangeActiveEditor.event; @@ -107,7 +134,7 @@ export class NotebookService extends Disposable implements INotebookService { super(); this._models = {}; - notebookExtensionPoint.setHandler((extensions) => { + notebookProviderExtensionPoint.setHandler((extensions) => { this.notebookProviderInfoStore.clear(); for (const extension of extensions) { @@ -122,6 +149,21 @@ export class NotebookService extends Disposable implements INotebookService { // console.log(this._notebookProviderInfoStore); }); + notebookRendererExtensionPoint.setHandler((renderers) => { + this.notebookRenderersInfoStore.clear(); + + for (const extension of renderers) { + for (const notebookContribution of extension.value) { + this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ + id: notebookContribution.viewType, + displayName: notebookContribution.displayName, + mimeTypes: notebookContribution.mimeTypes || [] + })); + } + } + + // console.log(this.notebookRenderersInfoStore); + }); } async canResolve(viewType: string): Promise { @@ -151,8 +193,8 @@ export class NotebookService extends Disposable implements INotebookService { this._notebookProviders.delete(viewType); } - registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, preloads: URI[]) { - this._notebookRenderers.set(handle, { extensionData, selectors, preloads }); + registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]) { + this._notebookRenderers.set(handle, { extensionData, type, selectors, preloads }); } unregisterNotebookRenderer(handle: number) { @@ -234,6 +276,10 @@ export class NotebookService extends Disposable implements INotebookService { return this.notebookProviderInfoStore.getContributedNotebook(resource); } + getContributedNotebookOutputRenderers(mimeType: string): readonly NotebookOutputRendererInfo[] { + return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); + } + getNotebookProviderResourceRoots(): URI[] { let ret: URI[] = []; this._notebookProviders.forEach(val => { diff --git a/src/vs/workbench/contrib/notebook/browser/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/output/outputRenderer.ts index 9c06ee6c863..4da7d769ed7 100644 --- a/src/vs/workbench/contrib/notebook/browser/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/output/outputRenderer.ts @@ -45,11 +45,11 @@ export class OutputRenderer { }; } - render(output: IOutput, container: HTMLElement): IRenderOutput { + render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { let transform = this._mimeTypeMapping[output.output_type]; if (transform) { - return transform.render(output, container); + return transform.render(output, container, preferredMimeType); } else { return this.renderNoop(output, container); } diff --git a/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts index 24d1a2f2b1e..b41350c5f63 100644 --- a/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts @@ -18,6 +18,7 @@ import { URI } from 'vs/base/common/uri'; class RichRenderer implements IOutputTransformContribution { private _mdRenderer: marked.Renderer = new marked.Renderer({ gfm: true });; + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( public notebookEditor: INotebookEditor, @@ -25,119 +26,158 @@ class RichRenderer implements IOutputTransformContribution { @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService ) { + this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); + this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this)); + this._richMimeTypeRenderers.set('text/html', this.renderHTML.bind(this)); + this._richMimeTypeRenderers.set('image/svg+xml', this.renderSVG.bind(this)); + this._richMimeTypeRenderers.set('text/markdown', this.renderMarkdown.bind(this)); + this._richMimeTypeRenderers.set('image/png', this.renderPNG.bind(this)); + this._richMimeTypeRenderers.set('image/jpeg', this.renderJavaScript.bind(this)); + this._richMimeTypeRenderers.set('text/plain', this.renderPlainText.bind(this)); } - render(output: any, container: HTMLElement): IRenderOutput { - let hasDynamicHeight = false; - - if (output.data) { - if (output.data['application/json']) { - let data = output.data['application/json']; - let str = JSON.stringify(data, null, '\t'); - - const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getJSONSimpleEditorOptions(), - dimension: { - width: 0, - height: 0 - } - }, { - isSimpleWidget: true - }); - - let mode = this.modeService.create('json'); - let resource = URI.parse(`notebook-output-${Date.now()}.json`); - const textModel = this.modelService.createModel(str, mode, resource, false); - editor.setModel(textModel); - - let width = this.notebookEditor.getListDimension()!.width; - let fontInfo = this.notebookEditor.getFontInfo(); - let height = Math.min(textModel.getLineCount(), 16) * (fontInfo?.lineHeight || 18); - - editor.layout({ - height, - width - }); - - container.style.height = `${height + 16}px`; - - return { - hasDynamicHeight: true - }; - } else if (output.data['application/javascript']) { - let data = output.data['application/javascript']; - let str = isArray(data) ? data.join('') : data; - let scriptVal = ``; - hasDynamicHeight = false; - return { - shadowContent: scriptVal, - hasDynamicHeight - }; - } else if (output.data['text/html']) { - let data = output.data['text/html']; - let str = isArray(data) ? data.join('') : data; - hasDynamicHeight = false; - return { - shadowContent: str, - hasDynamicHeight - }; - } else if (output.data['image/svg+xml']) { - let data = output.data['image/svg+xml']; - let str = isArray(data) ? data.join('') : data; - hasDynamicHeight = false; - return { - shadowContent: str, - hasDynamicHeight - }; - } else if (output.data['text/markdown']) { - let data = output.data['text/markdown']; - const str = isArray(data) ? data.join('') : data; - const mdOutput = document.createElement('div'); - mdOutput.innerHTML = marked(str, { renderer: this._mdRenderer }); - container.appendChild(mdOutput); - hasDynamicHeight = true; - } else if (output.data['image/png']) { - const image = document.createElement('img'); - image.src = `data:image/png;base64,${output.data['image/png']}`; - const display = document.createElement('div'); - DOM.addClasses(display, 'display'); - display.appendChild(image); - container.appendChild(display); - hasDynamicHeight = true; - } else if (output.data['image/jpeg']) { - const image = document.createElement('img'); - image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; - const display = document.createElement('div'); - DOM.addClasses(display, 'display'); - display.appendChild(image); - container.appendChild(display); - hasDynamicHeight = true; - } else if (output.data['text/plain']) { - let data = output.data['text/plain']; - let str = isArray(data) ? data.join('') : data; - const contentNode = document.createElement('p'); - contentNode.innerText = str; - container.appendChild(contentNode); - } else { - const contentNode = document.createElement('p'); - let mimeTypes = []; - for (const property in output.data) { - mimeTypes.push(property); - } - - let mimeTypesMessage = mimeTypes.join(', '); - - contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; - container.appendChild(contentNode); - } - } else { + render(output: any, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + if (!output.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); + + return { + hasDynamicHeight: false + }; } + if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { + const contentNode = document.createElement('p'); + let mimeTypes = []; + for (const property in output.data) { + mimeTypes.push(property); + } + + let mimeTypesMessage = mimeTypes.join(', '); + + contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + container.appendChild(contentNode); + + return { + hasDynamicHeight: false + }; + } + + let renderer = this._richMimeTypeRenderers.get(preferredMimeType); + return renderer!(output, container); + } + + renderJSON(output: any, container: HTMLElement) { + let data = output.data['application/json']; + let str = JSON.stringify(data, null, '\t'); + + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { + ...getJSONSimpleEditorOptions(), + dimension: { + width: 0, + height: 0 + } + }, { + isSimpleWidget: true + }); + + let mode = this.modeService.create('json'); + let resource = URI.parse(`notebook-output-${Date.now()}.json`); + const textModel = this.modelService.createModel(str, mode, resource, false); + editor.setModel(textModel); + + let width = this.notebookEditor.getListDimension()!.width; + let fontInfo = this.notebookEditor.getFontInfo(); + let height = Math.min(textModel.getLineCount(), 16) * (fontInfo?.lineHeight || 18); + + editor.layout({ + height, + width + }); + + container.style.height = `${height + 16}px`; + + return { + hasDynamicHeight: true + }; + } + + renderJavaScript(output: any, container: HTMLElement) { + let data = output.data['application/javascript']; + let str = isArray(data) ? data.join('') : data; + let scriptVal = ``; + return { + shadowContent: scriptVal, + hasDynamicHeight: false + }; + } + + renderHTML(output: any, container: HTMLElement) { + let data = output.data['text/html']; + let str = isArray(data) ? data.join('') : data; + return { + shadowContent: str, + hasDynamicHeight: false + }; + + } + + renderSVG(output: any, container: HTMLElement) { + let data = output.data['image/svg+xml']; + let str = isArray(data) ? data.join('') : data; + return { + shadowContent: str, + hasDynamicHeight: false + }; + } + + renderMarkdown(output: any, container: HTMLElement) { + let data = output.data['text/markdown']; + const str = isArray(data) ? data.join('') : data; + const mdOutput = document.createElement('div'); + mdOutput.innerHTML = marked(str, { renderer: this._mdRenderer }); + container.appendChild(mdOutput); + + return { + hasDynamicHeight: true + }; + } + + renderPNG(output: any, container: HTMLElement) { + const image = document.createElement('img'); + image.src = `data:image/png;base64,${output.data['image/png']}`; + const display = document.createElement('div'); + DOM.addClasses(display, 'display'); + display.appendChild(image); + container.appendChild(display); + return { + hasDynamicHeight: true + }; + + } + + renderJPEG(output: any, container: HTMLElement) { + const image = document.createElement('img'); + image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; + const display = document.createElement('div'); + DOM.addClasses(display, 'display'); + display.appendChild(image); + container.appendChild(display); + return { + hasDynamicHeight: true + }; + } + + renderPlainText(output: any, container: HTMLElement) { + let data = output.data['text/plain']; + let str = isArray(data) ? data.join('') : data; + const contentNode = document.createElement('p'); + contentNode.innerText = str; + container.appendChild(contentNode); + return { - hasDynamicHeight + hasDynamicHeight: false }; } diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts index 153bfd353cc..11eaa0c0aa0 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/cellRenderer.ts @@ -331,7 +331,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.showContextMenu(listIndex, element, e.posx, top + height); })); - elementDisposable?.add(new CodeCell(this.notebookEditor, element, templateData)); + elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); this.renderedEditors.set(element, templateData.editor); } diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts index 5031ff3882d..0d4234c2f76 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts @@ -3,21 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/renderers/sizeObserver'; -import { CELL_MARGIN, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN, IOutput, IDisplayOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; export class CodeCell extends Disposable { - private outputResizeListeners = new Map(); + private outputResizeListeners = new Map(); private outputElements = new Map(); constructor( private notebookEditor: INotebookEditor, private viewCell: CellViewModel, private templateData: CellRenderTemplate, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); @@ -65,6 +69,10 @@ export class CodeCell extends Disposable { viewCell.editorHeight = realContentHeight; } + + if (this.notebookEditor.getActiveCell() === this.viewCell) { + templateData.editor?.focus(); + } } }); @@ -187,8 +195,44 @@ export class CodeCell extends Disposable { } renderOutput(currOutput: IOutput, index: number, beforeElement?: HTMLElement) { + if (!this.outputResizeListeners.has(currOutput)) { + this.outputResizeListeners.set(currOutput, new DisposableStore()); + } + let outputItemDiv = document.createElement('div'); - let result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv); + let transformedOutput: IOutput; + let transformedMimeType: string; + if (currOutput.pickedMimeType) { + if (currOutput.transformedOutput && currOutput.transformedOutput![currOutput.pickedMimeType]) { + // currently, transformed output is always text/html + transformedMimeType = 'text/html'; + transformedOutput = currOutput.transformedOutput![currOutput.pickedMimeType]; + } else { + // otherwise + transformedMimeType = currOutput.pickedMimeType; + transformedOutput = currOutput; + } + } else { + transformedOutput = currOutput; + } + + if (currOutput.output_type === 'display_data' || currOutput.output_type === 'execute_result') { + let mimeTypes = Object.keys((currOutput! as IDisplayOutput).data); + + if (mimeTypes.length > 1) { + outputItemDiv.style.position = 'relative'; + const mimeTypePicker = DOM.$('.multi-mimetype-output'); + DOM.addClasses(mimeTypePicker, 'codicon', 'codicon-list-selection'); + outputItemDiv.appendChild(mimeTypePicker); + this.outputResizeListeners.get(currOutput)!.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeType(currOutput, mimeTypes); + })); + } + } + + let result = this.notebookEditor.getOutputRenderer().render(transformedOutput!, outputItemDiv, currOutput.pickedMimeType); if (!result) { this.viewCell.updateOutputHeight(index, 0); @@ -239,7 +283,7 @@ export class CodeCell extends Disposable { } }); elementSizeObserver.startObserving(); - this.outputResizeListeners.set(currOutput, elementSizeObserver); + this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); this.viewCell.updateOutputHeight(index, clientHeight); } else { if (result.shadowContent) { @@ -254,6 +298,64 @@ export class CodeCell extends Disposable { } } + async pickActiveMimeType(output: IOutput, mimeTypes: string[]) { + const sorted = mimeTypes.sort((a, b) => { + if (a === output.pickedMimeType) { + return -1; + } else { + return 0; + } + }); + + const items = sorted.map((mimeType): IQuickPickItem => ({ + label: mimeType, + id: mimeType, + description: mimeType === output.pickedMimeType + ? nls.localize('curruentActiveMimeType', "Currently Active") + : undefined, + // buttons: resourceExt ? [{ + // iconClass: 'codicon-settings-gear', + // tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt) + // }] : undefined + })); + + const picker = this.quickInputService.createQuickPick(); + picker.items = items; + picker.placeholder = nls.localize('promptChooseMimeType.placeHolder', "Select output mimetype to render for current output"); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0].id : undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (!pick) { + return; + } + + if (pick !== output.pickedMimeType) { + // user chooses another mimetype + let index = this.viewCell.outputs.indexOf(output); + let nextElement = index + 1 < this.viewCell.outputs.length ? this.outputElements.get(this.viewCell.outputs[index + 1]) : undefined; + this.outputResizeListeners.get(output)?.clear(); + let element = this.outputElements.get(output); + if (element) { + this.templateData?.outputContainer?.removeChild(element); + this.notebookEditor.removeInset(output); + } + + output.pickedMimeType = pick; + + this.renderOutput(output, index, nextElement); + + let editorHeight = this.viewCell.editorHeight; + let totalOutputHeight = this.viewCell.getOutputTotalHeight(); + this.notebookEditor.layoutNotebookCell(this.viewCell, editorHeight + 32 + totalOutputHeight); + } + } + dispose() { this.outputResizeListeners.forEach((value) => { value.dispose(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c913dfe1c64..4ecb503969d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -69,11 +69,11 @@ export interface IErrorOutput { * @internal */ export interface IDisplayOutput { - output_type: 'display_data'; + output_type: 'display_data' | 'execute_result'; /** * { mime_type: value } */ - data: { string: string }; + data: { [key: string]: any; } } /** @@ -82,7 +82,8 @@ export interface IDisplayOutput { export interface IGenericOutput { output_type: string; pickedMimeType?: string; - // transformedOutput?: IGenericOutput; + pickedRenderer?: number; + transformedOutput?: { [key: string]: IDisplayOutput }; } /** @@ -146,7 +147,7 @@ export interface IOutputTransformContribution { */ dispose(): void; - render(output: IOutput, container: HTMLElement): IRenderOutput; + render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts new file mode 100644 index 00000000000..796de2f51da --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as glob from 'vs/base/common/glob'; + +export class NotebookOutputRendererInfo { + + readonly id: string; + readonly displayName: string; + readonly mimeTypes: readonly string[]; + readonly mimeTypeGlobs: glob.ParsedPattern[]; + + constructor(descriptor: { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: readonly string[]; + }) { + this.id = descriptor.id; + this.displayName = descriptor.displayName; + this.mimeTypes = descriptor.mimeTypes; + this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); + } + + matches(mimeType: string) { + let matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); + return matched; + } +} -- GitLab