提交 d00f117c 编写于 作者: R rebornix

Mimetype picker first cut.

上级 09a81431
......@@ -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 ./",
......
......@@ -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: [
......
......@@ -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": {
......
......@@ -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 '<h1>kernel test renderer</h1>';
}
}
));
vscode.commands.registerCommand('notebook.saveToMarkdown', () => {
if (vscode.window.activeNotebookDocument) {
let document = vscode.window.activeNotebookDocument;
......
......@@ -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;
}
......
......@@ -228,8 +228,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
});
}
async $registerNotebookRenderer(extension: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void> {
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<void> {
this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri)));
}
async $unregisterNotebookRenderer(handle: number): Promise<void> {
......
......@@ -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;
......
......@@ -654,7 +654,7 @@ export type NotebookCellOutputsSplice = [
export interface MainThreadNotebookShape extends IDisposable {
$registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise<void>;
$unregisterNotebookProvider(viewType: string): Promise<void>;
$registerNotebookRenderer(extension: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void>;
$registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void>;
$unregisterNotebookRenderer(handle: number): Promise<void>;
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
......
......@@ -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);
}
(<IGenericOutput>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);
(<IGenericOutput>output).pickedRenderer = pickedHandler.handle;
(<IGenericOutput>output).transformedOutput = { richestMimeType: transformedOutput };
output = transformedOutput;
(<IGenericOutput>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(
......
......@@ -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<INotebookEditorContribution[]>({
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<INotebookEditorContribution[]>(
{
extensionPoint: 'notebookProvider',
jsonSchema: notebookProviderContribution
});
export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint<INotebookRendererContribution[]>(
{
extensionPoint: 'notebookOutputRenderer',
jsonSchema: notebookRendererContribution
});
......@@ -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;
}
......
......@@ -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<INotebook | undefined>;
......@@ -50,7 +51,7 @@ export interface INotebookService {
updateActiveNotebookDocument(viewType: string, resource: URI): void;
}
export class NotebookInfoStore {
export class NotebookProviderInfoStore {
private readonly contributedEditors = new Map<string, NotebookProviderInfo>();
clear() {
......@@ -75,6 +76,31 @@ export class NotebookInfoStore {
}
}
export class NotebookOutputRendererInfoStore {
private readonly contributedRenderers = new Map<string, NotebookOutputRendererInfo>();
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<string, { controller: IMainNotebookController, extensionData: NotebookExtensionDescription }>();
private readonly _notebookRenderers = new Map<number, { extensionData: NotebookExtensionDescription, selectors: INotebookMimeTypeSelector, preloads: URI[] }>();
notebookProviderInfoStore: NotebookInfoStore = new NotebookInfoStore();
private readonly _notebookRenderers = new Map<number, { extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[] }>();
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<void> {
......@@ -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 => {
......
......@@ -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);
}
......
......@@ -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<string, (output: any, container: HTMLElement) => 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 = `<script type="application/javascript">${str}</script>`;
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 = `<script type="application/javascript">${str}</script>`;
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
};
}
......
......@@ -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);
}
......
......@@ -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<IOutput, IDisposable>();
private outputResizeListeners = new Map<IOutput, DisposableStore>();
private outputElements = new Map<IOutput, HTMLElement>();
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<string | undefined>(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();
......
......@@ -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;
}
......
/*---------------------------------------------------------------------------------------------
* 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;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册