refactor: polish
- Fix mis-used dependencies. Add the roughly proposed 'extends' model for notebook renderers. Keep it out of contribution schema for now until we can work it more. - Made new loading work with JS modules. - Consolidated the 'old style' code in the renderer so that we can just delete it when the time comes. - Removed duplicated code and sharp edges from the 'queue' mechaism.
This commit is contained in:
parent
5c4fae9176
commit
616e0fd992
6 changed files with 264 additions and 240 deletions
|
@ -25,24 +25,18 @@
|
|||
{
|
||||
"id": "markdownItRenderer-katex",
|
||||
"displayName": "Markdown it katex renderer",
|
||||
"entrypoint": "./notebook-out/katex.js",
|
||||
"mimeTypes": [
|
||||
"text/markdown"
|
||||
],
|
||||
"dependencies": [
|
||||
"markdownItRenderer"
|
||||
]
|
||||
"entrypoint": {
|
||||
"extends": "markdownItRenderer",
|
||||
"path": "./notebook-out/katex.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "markdownItRenderer-emoji",
|
||||
"displayName": "Markdown it emoji renderer",
|
||||
"entrypoint": "./notebook-out/emoji.js",
|
||||
"mimeTypes": [
|
||||
"text/markdown"
|
||||
],
|
||||
"dependencies": [
|
||||
"markdownItRenderer"
|
||||
]
|
||||
"entrypoint": {
|
||||
"extends": "markdownItRenderer",
|
||||
"path": "./notebook-out/emoji.js"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { NotebookEditorPriority, NotebookRendererEntrypoint } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
|
||||
namespace NotebookEditorContribution {
|
||||
export const viewType = 'viewType';
|
||||
|
@ -37,7 +37,7 @@ export interface INotebookRendererContribution {
|
|||
readonly [NotebookRendererContribution.viewType]?: string;
|
||||
readonly [NotebookRendererContribution.displayName]: string;
|
||||
readonly [NotebookRendererContribution.mimeTypes]?: readonly string[];
|
||||
readonly [NotebookRendererContribution.entrypoint]: string;
|
||||
readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint;
|
||||
readonly [NotebookRendererContribution.hardDependencies]: readonly string[];
|
||||
readonly [NotebookRendererContribution.optionalDependencies]: readonly string[];
|
||||
}
|
||||
|
@ -130,8 +130,27 @@ const notebookRendererContribution: IJSONSchema = {
|
|||
}
|
||||
},
|
||||
[NotebookRendererContribution.entrypoint]: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
// todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption
|
||||
// {
|
||||
// type: 'object',
|
||||
// required: ['extends', 'path'],
|
||||
// properties: {
|
||||
// extends: {
|
||||
// type: 'string',
|
||||
// description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'),
|
||||
// },
|
||||
// path: {
|
||||
// type: 'string',
|
||||
// description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
]
|
||||
},
|
||||
[NotebookRendererContribution.hardDependencies]: {
|
||||
type: 'array',
|
||||
|
|
|
@ -203,7 +203,7 @@ export interface ICreationRequestMessage {
|
|||
cellTop: number;
|
||||
outputOffset: number;
|
||||
left: number;
|
||||
requiredPreloads: ReadonlyArray<IPreloadResource>;
|
||||
requiredPreloads: ReadonlyArray<IControllerPreload>;
|
||||
readonly initiallyHidden?: boolean;
|
||||
rendererId?: string | undefined;
|
||||
}
|
||||
|
@ -263,17 +263,15 @@ export interface IAckOutputHeightMessage {
|
|||
height: number;
|
||||
}
|
||||
|
||||
export type PreloadSource = 'kernel' | { rendererId: string };
|
||||
|
||||
export interface IPreloadResource {
|
||||
export interface IControllerPreload {
|
||||
originalUri: string;
|
||||
uri: string;
|
||||
source: PreloadSource;
|
||||
}
|
||||
|
||||
export interface IUpdatePreloadResourceMessage {
|
||||
export interface IUpdateControllerPreloadsMessage {
|
||||
type: 'preload';
|
||||
resources: IPreloadResource[];
|
||||
resources: IControllerPreload[];
|
||||
}
|
||||
|
||||
export interface IUpdateDecorationsMessage {
|
||||
|
@ -376,7 +374,7 @@ export type ToWebviewMessage =
|
|||
| IClearOutputRequestMessage
|
||||
| IHideOutputMessage
|
||||
| IShowOutputMessage
|
||||
| IUpdatePreloadResourceMessage
|
||||
| IUpdateControllerPreloadsMessage
|
||||
| IUpdateDecorationsMessage
|
||||
| ICustomKernelMessage
|
||||
| ICreateMarkdownMessage
|
||||
|
@ -767,7 +765,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
|
|||
id: renderer.id,
|
||||
entrypoint,
|
||||
mimeTypes: renderer.mimeTypes,
|
||||
dependencies: Array.from(renderer.dependencies.values())
|
||||
extends: renderer.extends,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1194,7 +1192,6 @@ var requirejs = (function() {
|
|||
if (this._currentKernel) {
|
||||
this._updatePreloadsFromKernel(this._currentKernel);
|
||||
}
|
||||
this.updateRendererPreloads(renderers);
|
||||
|
||||
for (const [output, inset] of this.insetMapping.entries()) {
|
||||
this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) });
|
||||
|
@ -1469,7 +1466,6 @@ var requirejs = (function() {
|
|||
...messageBase,
|
||||
outputId: output.outputId,
|
||||
rendererId: content.renderer.id,
|
||||
requiredPreloads: await this.updateRendererPreloads([content.renderer]),
|
||||
content: {
|
||||
type: RenderOutputType.Extension,
|
||||
outputId: output.outputId,
|
||||
|
@ -1600,13 +1596,13 @@ var requirejs = (function() {
|
|||
}
|
||||
|
||||
private _updatePreloadsFromKernel(kernel: INotebookKernel) {
|
||||
const resources: IPreloadResource[] = [];
|
||||
const resources: IControllerPreload[] = [];
|
||||
for (const preload of kernel.preloadUris) {
|
||||
const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https')
|
||||
? preload : this.asWebviewUri(preload, undefined);
|
||||
|
||||
if (!this._preloadsCache.has(uri.toString())) {
|
||||
resources.push({ uri: uri.toString(), originalUri: preload.toString(), source: 'kernel' });
|
||||
resources.push({ uri: uri.toString(), originalUri: preload.toString() });
|
||||
this._preloadsCache.add(uri.toString());
|
||||
}
|
||||
}
|
||||
|
@ -1618,43 +1614,7 @@ var requirejs = (function() {
|
|||
this._updatePreloads(resources);
|
||||
}
|
||||
|
||||
async updateRendererPreloads(renderers: Iterable<INotebookRendererInfo>) {
|
||||
if (this._disposed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requiredPreloads: IPreloadResource[] = [];
|
||||
const resources: IPreloadResource[] = [];
|
||||
const extensionLocations: URI[] = [];
|
||||
for (const rendererInfo of renderers) {
|
||||
extensionLocations.push(rendererInfo.extensionLocation);
|
||||
for (const preload of [rendererInfo.entrypoint, ...rendererInfo.preloads]) {
|
||||
const uri = this.asWebviewUri(preload, rendererInfo.extensionLocation);
|
||||
const resource: IPreloadResource = {
|
||||
uri: uri.toString(),
|
||||
originalUri: preload.toString(),
|
||||
source: { rendererId: rendererInfo.id },
|
||||
};
|
||||
|
||||
requiredPreloads.push(resource);
|
||||
|
||||
if (!this._preloadsCache.has(uri.toString())) {
|
||||
resources.push(resource);
|
||||
this._preloadsCache.add(uri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resources.length) {
|
||||
return requiredPreloads;
|
||||
}
|
||||
|
||||
this.rendererRootsCache = extensionLocations;
|
||||
this._updatePreloads(resources);
|
||||
return requiredPreloads;
|
||||
}
|
||||
|
||||
private _updatePreloads(resources: IPreloadResource[]) {
|
||||
private _updatePreloads(resources: IControllerPreload[]) {
|
||||
if (!this.webview) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -129,10 +129,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
}
|
||||
|
||||
function createRendererContext(rendererId: string): RendererContext {
|
||||
const api = acquireNotebookRendererApi<any>(rendererId);
|
||||
return {
|
||||
getState: api.getState.bind(api),
|
||||
setState: api.setState.bind(api),
|
||||
setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }),
|
||||
getState: <T>() => {
|
||||
const state = vscode.getState();
|
||||
return typeof state === 'object' && state ? state[rendererId] as T : undefined;
|
||||
},
|
||||
getRenderer: (id: string) => renderers.getRenderer(id),
|
||||
};
|
||||
}
|
||||
|
@ -162,16 +164,39 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
if (isModule) {
|
||||
return __import(url);
|
||||
} else {
|
||||
return createBackCompatModule(rendererId, text);
|
||||
}
|
||||
};
|
||||
|
||||
const createBackCompatModule = (rendererId: string, scriptText: string): ScriptModule => ({
|
||||
activate: (): RendererApi => {
|
||||
const onDidCreateOutput = createEmitter<ICreateCellInfo>();
|
||||
const onWillDestroyOutput = createEmitter<undefined | IDestroyCellInfo>();
|
||||
|
||||
const globals = {
|
||||
acquireNotebookRendererApi: <T>(): GlobalNotebookRendererApi<T> => ({
|
||||
onDidCreateOutput: onDidCreateOutput.event,
|
||||
onWillDestroyOutput: onWillDestroyOutput.event,
|
||||
setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }),
|
||||
getState: () => {
|
||||
const state = vscode.getState();
|
||||
return typeof state === 'object' && state ? state[rendererId] as T : undefined;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
invokeSourceWithGlobals(scriptText, globals);
|
||||
|
||||
return {
|
||||
activate: () => {
|
||||
const globals = {
|
||||
acquireNotebookRendererApi: () => acquireNotebookRendererApi(rendererId)
|
||||
};
|
||||
return invokeSourceWithGlobals(text, globals);
|
||||
renderCell(id, context) {
|
||||
onDidCreateOutput.fire({ ...context, outputId: id });
|
||||
},
|
||||
destroyCell(id) {
|
||||
onWillDestroyOutput.fire(id ? { outputId: id } : undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const dimensionUpdater = new class {
|
||||
private readonly pending = new Map<string, DimensionUpdate>();
|
||||
|
@ -388,8 +413,6 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
focusTrackers.set(outputId, new FocusTracker(element, outputId));
|
||||
}
|
||||
|
||||
const dontEmit = Symbol('dontEmit');
|
||||
|
||||
function createEmitter<T>(listenerChange: (listeners: Set<Listener<T>>) => void = () => undefined): EmitterLike<T> {
|
||||
const listeners = new Set<Listener<T>>();
|
||||
return {
|
||||
|
@ -421,24 +444,16 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
};
|
||||
}
|
||||
|
||||
// Maps the events in the given emitter, invoking mapFn on each one. mapFn can return
|
||||
// the dontEmit symbol to skip emission.
|
||||
function mapEmitter<T, R>(emitter: EmitterLike<T>, mapFn: (data: T) => R | typeof dontEmit) {
|
||||
let listener: IDisposable;
|
||||
const mapped = createEmitter<R>(listeners => {
|
||||
if (listeners.size && !listener) {
|
||||
listener = emitter.event(data => {
|
||||
const v = mapFn(data);
|
||||
if (v !== dontEmit) {
|
||||
mapped.fire(v);
|
||||
}
|
||||
});
|
||||
} else if (listener && !listeners.size) {
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return mapped.event;
|
||||
function showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) {
|
||||
outputNode.innerText = `Error loading preloads:`;
|
||||
const errList = document.createElement('ul');
|
||||
for (const result of errors) {
|
||||
console.error(result);
|
||||
const item = document.createElement('li');
|
||||
item.innerText = result.message;
|
||||
errList.appendChild(item);
|
||||
}
|
||||
outputNode.appendChild(errList);
|
||||
}
|
||||
|
||||
interface ICreateCellInfo {
|
||||
|
@ -454,10 +469,9 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
outputId: string;
|
||||
}
|
||||
|
||||
const onWillDestroyOutput = createEmitter<'all' | { rendererId: string, info: IDestroyCellInfo }>();
|
||||
const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>();
|
||||
const onDidReceiveKernelMessage = createEmitter<unknown>();
|
||||
|
||||
/** @deprecated */
|
||||
interface GlobalNotebookRendererApi<T> {
|
||||
setState: (newState: T) => void;
|
||||
getState(): T | undefined;
|
||||
|
@ -465,65 +479,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
readonly onDidCreateOutput: Event<ICreateCellInfo>;
|
||||
}
|
||||
|
||||
const acquireNotebookRendererApi = <T>(id: string): GlobalNotebookRendererApi<T> => ({
|
||||
setState(newState: T) {
|
||||
vscode.setState({ ...vscode.getState(), [id]: newState });
|
||||
},
|
||||
getState(): T | undefined {
|
||||
const state = vscode.getState();
|
||||
return typeof state === 'object' && state ? state[id] as T : undefined;
|
||||
},
|
||||
onWillDestroyOutput: mapEmitter(onWillDestroyOutput, (evt) => {
|
||||
if (evt === 'all') {
|
||||
return undefined;
|
||||
}
|
||||
return evt.rendererId === id ? evt.info : dontEmit;
|
||||
}),
|
||||
onDidCreateOutput: mapEmitter(onDidCreateOutput, ({ rendererId, info }) => rendererId === id ? info : dontEmit),
|
||||
});
|
||||
|
||||
const kernelPreloadGlobals = {
|
||||
acquireVsCodeApi,
|
||||
onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
|
||||
postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }),
|
||||
};
|
||||
|
||||
const enum PreloadState {
|
||||
Ok,
|
||||
Error
|
||||
}
|
||||
|
||||
type PreloadResult = { state: PreloadState.Ok } | { state: PreloadState.Error, error: string };
|
||||
|
||||
/**
|
||||
* Map of preload resource URIs to promises that resolve one the resource
|
||||
* loads or errors.
|
||||
*/
|
||||
const preloadPromises = new Map<string, Promise<PreloadResult>>();
|
||||
const queuedOuputActions = new Map<string, Promise<void>>();
|
||||
|
||||
/**
|
||||
* Enqueues an action that affects a output. This blocks behind renderer load
|
||||
* requests that affect the same output. This should be called whenever you
|
||||
* do something that affects output to ensure it runs in
|
||||
* the correct order.
|
||||
*/
|
||||
const enqueueOutputAction = <T extends { outputId: string; }>(event: T, fn: (event: T) => Promise<void> | void) => {
|
||||
const queued = queuedOuputActions.get(event.outputId);
|
||||
const maybePromise = queued ? queued.then(() => fn(event)) : fn(event);
|
||||
if (typeof maybePromise === 'undefined') {
|
||||
return; // a synchonrously-called function, we're done
|
||||
}
|
||||
|
||||
const promise = maybePromise.then(() => {
|
||||
if (queuedOuputActions.get(event.outputId) === promise) {
|
||||
queuedOuputActions.delete(event.outputId);
|
||||
}
|
||||
});
|
||||
|
||||
queuedOuputActions.set(event.outputId, promise);
|
||||
};
|
||||
|
||||
const ttPolicy = window.trustedTypes?.createPolicy('notebookOutputRenderer', {
|
||||
createHTML: value => value,
|
||||
createScript: value => value,
|
||||
|
@ -605,10 +566,15 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 'html':
|
||||
enqueueOutputAction(event.data, async data => {
|
||||
const preloadResults = await Promise.all(data.requiredPreloads.map(p => preloadPromises.get(p.uri)));
|
||||
if (!queuedOuputActions.has(data.outputId)) { // output was cleared while loading
|
||||
case 'html': {
|
||||
const data = event.data;
|
||||
outputs.enqueue(event.data.outputId, async (state) => {
|
||||
const preloadsAndErrors = await Promise.all<unknown>([
|
||||
data.rendererId ? renderers.load(data.rendererId) : undefined,
|
||||
...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)),
|
||||
].map(p => p?.catch(err => err)));
|
||||
|
||||
if (state.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -658,38 +624,26 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
if (content.type === RenderOutputType.Html) {
|
||||
const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent;
|
||||
outputNode.innerHTML = trustedHtml as string;
|
||||
cellOutputContainer.appendChild(outputContainer);
|
||||
outputContainer.appendChild(outputNode);
|
||||
domEval(outputNode);
|
||||
} else if (preloadResults.some(e => e?.state === PreloadState.Error)) {
|
||||
outputNode.innerText = `Error loading preloads:`;
|
||||
const errList = document.createElement('ul');
|
||||
for (const result of preloadResults) {
|
||||
if (result?.state === PreloadState.Error) {
|
||||
const item = document.createElement('li');
|
||||
item.innerText = result.error;
|
||||
errList.appendChild(item);
|
||||
}
|
||||
}
|
||||
outputNode.appendChild(errList);
|
||||
cellOutputContainer.appendChild(outputContainer);
|
||||
outputContainer.appendChild(outputNode);
|
||||
} else if (preloadsAndErrors.some(e => e instanceof Error)) {
|
||||
const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error);
|
||||
showPreloadErrors(outputNode, ...errors);
|
||||
} else {
|
||||
// TODO: this should go through renderers instead
|
||||
onDidCreateOutput.fire({
|
||||
rendererId: data.rendererId!,
|
||||
info: {
|
||||
const rendererApi = preloadsAndErrors[0] as RendererApi;
|
||||
try {
|
||||
rendererApi.renderCell(outputId, {
|
||||
element: outputNode,
|
||||
outputId,
|
||||
mime: content.mimeType,
|
||||
value: content.value,
|
||||
metadata: content.metadata,
|
||||
}
|
||||
});
|
||||
cellOutputContainer.appendChild(outputContainer);
|
||||
outputContainer.appendChild(outputNode);
|
||||
});
|
||||
} catch (e) {
|
||||
showPreloadErrors(outputNode, e);
|
||||
}
|
||||
}
|
||||
|
||||
cellOutputContainer.appendChild(outputContainer);
|
||||
outputContainer.appendChild(outputNode);
|
||||
resizeObserver.observe(outputNode, outputId, true);
|
||||
|
||||
const clientHeight = outputNode.clientHeight;
|
||||
|
@ -714,6 +668,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
cellOutputContainer.style.visibility = data.initiallyHidden ? 'hidden' : 'visible';
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'view-scroll':
|
||||
{
|
||||
// const date = new Date();
|
||||
|
@ -740,8 +695,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
break;
|
||||
}
|
||||
case 'clear':
|
||||
queuedOuputActions.clear(); // stop all loading outputs
|
||||
onWillDestroyOutput.fire('all');
|
||||
renderers.clearAll();
|
||||
document.getElementById('container')!.innerText = '';
|
||||
|
||||
focusTrackers.forEach(ft => {
|
||||
|
@ -753,26 +707,29 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
const output = document.getElementById(event.data.outputId);
|
||||
const { rendererId, outputId } = event.data;
|
||||
|
||||
queuedOuputActions.delete(outputId); // stop any in-progress rendering
|
||||
outputs.cancelOutput(outputId);
|
||||
if (output && output.parentNode) {
|
||||
if (rendererId) {
|
||||
onWillDestroyOutput.fire({ rendererId, info: { outputId } });
|
||||
renderers.clearOutput(rendererId, outputId);
|
||||
}
|
||||
output.parentNode.removeChild(output);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'hideOutput':
|
||||
enqueueOutputAction(event.data, ({ outputId }) => {
|
||||
case 'hideOutput': {
|
||||
const { outputId } = event.data;
|
||||
outputs.enqueue(event.data.outputId, () => {
|
||||
const container = document.getElementById(outputId)?.parentElement?.parentElement;
|
||||
if (container) {
|
||||
container.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'showOutput':
|
||||
enqueueOutputAction(event.data, ({ outputId, cellTop: top, }) => {
|
||||
}
|
||||
case 'showOutput': {
|
||||
const { outputId, cellTop: top } = event.data;
|
||||
outputs.enqueue(event.data.outputId, () => {
|
||||
const output = document.getElementById(outputId);
|
||||
if (output) {
|
||||
output.parentElement!.parentElement!.style.visibility = 'visible';
|
||||
|
@ -784,6 +741,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'ack-dimension':
|
||||
{
|
||||
const { outputId, height } = event.data;
|
||||
|
@ -796,35 +754,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
}
|
||||
case 'preload':
|
||||
const resources = event.data.resources;
|
||||
let queue: Promise<PreloadResult> = Promise.resolve({ state: PreloadState.Ok });
|
||||
for (const { uri, originalUri, source } of resources) {
|
||||
// create the promise so that the scripts download in parallel, but
|
||||
// only invoke them in series within the queue
|
||||
|
||||
if (source === 'kernel') {
|
||||
const promise = runPreload(uri, originalUri);
|
||||
queue = queue.then(() => promise.then(async module => {
|
||||
try {
|
||||
await module.activate();
|
||||
return { state: PreloadState.Ok };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { state: PreloadState.Error, error: error.toString() };
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
queue = queue.then(async () => {
|
||||
try {
|
||||
await renderers.load(source.rendererId);
|
||||
return { state: PreloadState.Ok };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { state: PreloadState.Error, error: error.toString() };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
preloadPromises.set(uri, queue);
|
||||
for (const { uri, originalUri } of resources) {
|
||||
kernelPreloads.load(uri, originalUri);
|
||||
}
|
||||
break;
|
||||
case 'focus-output':
|
||||
|
@ -863,12 +794,13 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
|
||||
interface RendererApi {
|
||||
renderCell: (id: string, context: ICreateCellInfo) => void;
|
||||
destroyCell?: (id?: string) => void;
|
||||
}
|
||||
|
||||
class Renderer {
|
||||
constructor(
|
||||
public readonly data: RendererMetadata,
|
||||
private readonly loadDependency: (id: string) => Promise<void>,
|
||||
private readonly loadExtension: (id: string) => Promise<void>,
|
||||
) { }
|
||||
|
||||
private _loadPromise: Promise<RendererApi> | undefined;
|
||||
|
@ -878,33 +810,116 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
|
||||
public load(): Promise<RendererApi | undefined> {
|
||||
if (!this._loadPromise) {
|
||||
this._loadPromise = Promise.all(this.data.dependencies.map(dependencyId => this.loadDependency(dependencyId)))
|
||||
.then(() => runRenderScript(this.data.entrypoint, this.data.id))
|
||||
.then(module => {
|
||||
if (module) {
|
||||
const api = module.activate(createRendererContext(this.data.id));
|
||||
this._api = api;
|
||||
return api;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
this._loadPromise = this._load();
|
||||
}
|
||||
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
/** Inner function cached in the _loadPromise(). */
|
||||
private async _load() {
|
||||
const module = await runRenderScript(this.data.entrypoint, this.data.id);
|
||||
if (!module) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = module.activate(createRendererContext(this.data.id));
|
||||
this._api = api;
|
||||
|
||||
// Squash any errors extends errors. They won't prevent the renderer
|
||||
// itself from working, so just log them.
|
||||
await Promise.all(rendererData
|
||||
.filter(d => d.extends === this.data.id)
|
||||
.map(d => this.loadExtension(d.id).catch(console.error)),
|
||||
);
|
||||
|
||||
return api;
|
||||
}
|
||||
}
|
||||
|
||||
const renderers = new class {
|
||||
const kernelPreloads = new class {
|
||||
private readonly preloads = new Map<string /* uri */, Promise<ScriptModule>>();
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the given preload is activated.
|
||||
*/
|
||||
public waitFor(uri: string) {
|
||||
return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a preload.
|
||||
* @param uri URI to load from
|
||||
* @param originalUri URI to show in an error message if the preload is invalid.
|
||||
*/
|
||||
public load(uri: string, originalUri: string) {
|
||||
const promise = Promise.all([
|
||||
runPreload(uri, originalUri),
|
||||
this.waitForAllCurrent(),
|
||||
]).then(([module]) => module.activate());
|
||||
|
||||
this.preloads.set(uri, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that waits for all currently-registered preloads to
|
||||
* activate before resolving.
|
||||
*/
|
||||
private waitForAllCurrent() {
|
||||
return Promise.all([...this.preloads.values()].map(p => p.catch(err => err)));
|
||||
}
|
||||
};
|
||||
|
||||
const outputs = new class {
|
||||
private outputs = new Map<string, { cancelled: boolean; queue: Promise<unknown> }>();
|
||||
/**
|
||||
* Pushes the action onto the list of actions for the given output ID,
|
||||
* ensuring that it's run in-order.
|
||||
*/
|
||||
public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) {
|
||||
const record = this.outputs.get(outputId);
|
||||
if (!record) {
|
||||
this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) });
|
||||
} else {
|
||||
record.queue = record.queue.then(r => !record.cancelled && action(record));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancells the rendering of all outputs.
|
||||
*/
|
||||
public cancelAll() {
|
||||
for (const record of this.outputs.values()) {
|
||||
record.cancelled = true;
|
||||
}
|
||||
this.outputs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any ongoing rendering out an output.
|
||||
*/
|
||||
public cancelOutput(outputId: string) {
|
||||
const output = this.outputs.get(outputId);
|
||||
if (output) {
|
||||
output.cancelled = true;
|
||||
this.outputs.delete(outputId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderers = new class {
|
||||
private readonly _renderers = new Map</* id */ string, Renderer>();
|
||||
|
||||
constructor() {
|
||||
for (const renderer of rendererData) {
|
||||
this._renderers.set(renderer.id, new Renderer(renderer, async (dependencyId) => {
|
||||
const parent = this._renderers.get(dependencyId);
|
||||
if (!parent) {
|
||||
throw new Error(`Could not find renderer dependency: ${dependencyId}`);
|
||||
this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => {
|
||||
const ext = this._renderers.get(extensionId);
|
||||
if (!ext) {
|
||||
throw new Error(`Could not find extending renderer: ${extensionId}`);
|
||||
}
|
||||
await parent.load();
|
||||
|
||||
await ext.load();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -913,7 +928,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
return this._renderers.get(id)?.api;
|
||||
}
|
||||
|
||||
public load(id: string) {
|
||||
public async load(id: string) {
|
||||
const renderer = this._renderers.get(id);
|
||||
if (!renderer) {
|
||||
throw new Error('Could not find renderer');
|
||||
|
@ -922,9 +937,31 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
return renderer.load();
|
||||
}
|
||||
|
||||
|
||||
public clearAll() {
|
||||
outputs.cancelAll();
|
||||
for (const renderer of this._renderers.values()) {
|
||||
renderer.api?.destroyCell?.();
|
||||
}
|
||||
}
|
||||
|
||||
public clearOutput(rendererId: string, outputId: string) {
|
||||
outputs.cancelOutput(outputId);
|
||||
this._renderers.get(rendererId)?.api?.destroyCell?.(outputId);
|
||||
}
|
||||
|
||||
public async renderCustom(rendererId: string, outputId: string, info: ICreateCellInfo) {
|
||||
const api = await this.load(rendererId);
|
||||
if (!api) {
|
||||
throw new Error(`renderer ${rendererId} did not return an API`);
|
||||
}
|
||||
|
||||
api.renderCell(outputId, info);
|
||||
}
|
||||
|
||||
public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise<void> {
|
||||
const markdownRenderers = Array.from(this._renderers.values())
|
||||
.filter(renderer => renderer.data.mimeTypes.includes('text/markdown'));
|
||||
.filter(renderer => renderer.data.mimeTypes.includes('text/markdown') && !renderer.data.extends);
|
||||
|
||||
if (!markdownRenderers.length) {
|
||||
throw new Error('Could not find renderer');
|
||||
|
@ -932,8 +969,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
|
||||
await Promise.all(markdownRenderers.map(x => x.load()));
|
||||
|
||||
const renderer = Array.from(this._renderers.values()).find(x => x.data.mimeTypes.includes('text/markdown'));
|
||||
renderer?.api?.renderCell(id, {
|
||||
markdownRenderers[0].api?.renderCell(id, {
|
||||
element,
|
||||
value: content,
|
||||
mime: 'text/markdown',
|
||||
|
@ -1172,8 +1208,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend
|
|||
export interface RendererMetadata {
|
||||
readonly id: string;
|
||||
readonly entrypoint: string;
|
||||
readonly dependencies: readonly string[]
|
||||
readonly mimeTypes: readonly string[];
|
||||
readonly extends: string | undefined;
|
||||
}
|
||||
|
||||
export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) {
|
||||
|
|
|
@ -53,6 +53,8 @@ export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [
|
|||
export const BUILTIN_RENDERER_ID = '_builtin';
|
||||
export const RENDERER_NOT_AVAILABLE = '_notAvailable';
|
||||
|
||||
export type NotebookRendererEntrypoint = string | { extends: string; path: string };
|
||||
|
||||
export enum NotebookRunState {
|
||||
Running = 1,
|
||||
Idle = 2
|
||||
|
@ -132,6 +134,7 @@ export const enum NotebookRendererMatch {
|
|||
export interface INotebookRendererInfo {
|
||||
id: string;
|
||||
displayName: string;
|
||||
extends?: string;
|
||||
entrypoint: URI;
|
||||
preloads: ReadonlyArray<URI>;
|
||||
extensionLocation: URI;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Iterable } from 'vs/base/common/iterator';
|
|||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
|
||||
class DependencyList {
|
||||
private readonly value: ReadonlySet<string>;
|
||||
|
@ -34,6 +34,7 @@ class DependencyList {
|
|||
export class NotebookOutputRendererInfo implements INotebookRendererInfo {
|
||||
|
||||
readonly id: string;
|
||||
readonly extends?: string;
|
||||
readonly entrypoint: URI;
|
||||
readonly displayName: string;
|
||||
readonly extensionLocation: URI;
|
||||
|
@ -49,7 +50,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo {
|
|||
constructor(descriptor: {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly entrypoint: string;
|
||||
readonly entrypoint: NotebookRendererEntrypoint;
|
||||
readonly mimeTypes: readonly string[];
|
||||
readonly extension: IExtensionDescription;
|
||||
readonly dependencies: readonly string[] | undefined;
|
||||
|
@ -58,7 +59,14 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo {
|
|||
this.id = descriptor.id;
|
||||
this.extensionId = descriptor.extension.identifier;
|
||||
this.extensionLocation = descriptor.extension.extensionLocation;
|
||||
this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint);
|
||||
|
||||
if (typeof descriptor.entrypoint === 'string') {
|
||||
this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint);
|
||||
} else {
|
||||
this.extends = descriptor.entrypoint.extends;
|
||||
this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint.path);
|
||||
}
|
||||
|
||||
this.displayName = descriptor.displayName;
|
||||
this.mimeTypes = descriptor.mimeTypes;
|
||||
this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern));
|
||||
|
@ -103,6 +111,10 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo {
|
|||
}
|
||||
|
||||
private matchesMimeTypeOnly(mimeType: string) {
|
||||
if (this.extends !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) || this.mimeTypes.some(pattern => pattern === mimeType);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue