Description
Last iteration we built a prototype for rendering Notebook in the core and based on the lessons we learnt while dealing with Notebook outputs, we found that Notebook outputs can be arbitrary, it can contain text, streams, images, html and scripts. More over, some of the output types expect them to be in the same browser environment and can access the DOM structure directly. It leads to challenges of rendering outputs in the core as we can't just insert them into the UI. Out current approach is rendering insecure outputs in a web view and lay code cells on top of the web view.
This month, we will set up a test extension which can emit different types of outputs (acting like a mock Notebook kernel) and we hook the extension up with the core. We will use this extension to test output rendering and figure out what's the missing part with current implementation.
The work is happening now in #86632.
Notebook rendering improvement and testing
The rendering approach we take here is rendering secure outputs in the core (like markdown, text, ansi) and insecure ones in webview. The core will sync their positioning and dimensions.
Our major focus is improving the scrolling experience for insecure outputs and exploring how we can support full Jupyter notebook outputs (nteract, ipywidgets) but still keep VS Code core free of any Jupyter Notebook concept.
- Proposed API for Notebook providers registration and notebook execution
- Limitations (common scripts loading/injection): we now support outputs with
<script>
tags.
- Limitations (common scripts loading/injection): we now support outputs with
- Output rendering (safe outputs rendered in core)
- Basic elements
- Text/Stream
- ANSI output
- Images
- PNG
- Others?
- Basic elements
- Output rendering (insecure ones, rendered in webview)
- HTML elements
- static elements
- script tags
- We will create script tags and add them to the output container to trigger the browser download and evaluate them.
- ipywidgets
- ipywidgets requires widget manager to be created and manages states for both models and views.
- rendering ipywidgets requires third party libraries but they should be transparent to the core.
- ✨ extensions can create js bundles for ipywidgets and its widget manager and sent them back as outputs
- nteract: https://components.nteract.io/#section-nteractoutputsmedia
- Vega/Vega-lite
- HTML elements
API exploration
To test different types of notebook outputs, we started the sketch of notebook API. The first prototype was as simple as a language provider, which can provide content for a notebook opened in the editor:
export interface NotebookProvider {
onDidChangeNotebook?: Event<{ resource: Uri, notebook: INotebook }>;
resolveNotebook(resource: Uri): Promise<INotebook | undefined>;
executeNotebook(resource: Uri): Promise<void>;
}
namespace window {
export function registerNotebookProvider(
notebookType: string,
provider: NotebookProvider
): Disposable;
}
As we noted in #86632, a notebook document consists of code cells and outputs. Every code cell is a text document and will be loaded in Monaco Editor when visible. A notebook document should have following functionalities:
- store and provide access to code cells (what is a code cell here?
string | string[]
orvscode.TextDocument
?) - support saving for code cells
- update outputs and emit change events
- support notebook or code cell execution
- support notebook incremental update
The challenge here is how to describe a code cell: how will notebook provide generate them, how to access their latest content when they are updated in the UI, etc.
Alternative implementation
The major UX challenge with current approach is we are syncing notebook scrolling and notebook cells/outputs sizes between the core UI and the web view. Users can see the lagging in the webview while scrolling the editor. We will continue with current approach and see how far we can go but we also need to look into alternative implementations.
Notes
RequireJS/AMD
Jupyter uses RequireJS to handle JavaScript libraries and the whole ecosystem seems be on top of it (correct me if I'm wrong). With RequireJs or any AMD loader, notebooks can load dependencies dynamically and this is a fundamental infrastructure for interactive outputs, like ipywidgets and Vega.
While implementing the notebook rendering in the core, we still want to keep the core clean and free of Jupyter knowledge, especially how and what Jupyter needs to render outputs. The JavaScript dependencies an output requires should all come from notebook providers.
VS Code has a builtin AMD loader and we can inject this loader to Output rendering environment in advance. Jupyter Notebook providers can take it for granted (assuming that there is always a AMD loader so Notebook providers don't need to inject RequireJS themselves). The heavy lifting work for Jupyter Notebook providers is declaring dynamic dependencies it needs for rendering an interactive output. Thus some output marshaling is required.
Let's use Vega as an example, the outputs we receive from Jupyter Notebook is similar to below
const spec = {
"$schema": "https://vega.github.io/schema/vega/v5.json", "width": 400, "height": 200, "padding": 5, "data": [{ "name": "table", "values": [{ "category": "A", "amount": 28 }, { "category": "B", "amount": 55 }, { "category": "C", "amount": 43 }, { "category": "D", "amount": 91 }, { "category": "E", "amount": 81 }, { "category": "F", "amount": 53 }, { "category": "G", "amount": 19 }, { "category": "H", "amount": 87 }] }], "signals": [{ "name": "tooltip", "value": {}, "on": [{ "events": "rect:mouseover", "update": "datum" }, { "events": "rect:mouseout", "update": "{}" }] }], "scales": [{ "name": "xscale", "type": "band", "domain": { "data": "table", "field": "category" }, "range": "width", "padding": 0.05, "round": true }, { "name": "yscale", "domain": { "data": "table", "field": "amount" }, "nice": true, "range": "height" }], "axes": [{ "orient": "bottom", "scale": "xscale" }, { "orient": "left", "scale": "yscale" }], "marks": [{ "type": "rect", "from": { "data": "table" }, "encode": { "enter": { "x": { "scale": "xscale", "field": "category" }, "width": { "scale": "xscale", "band": 1 }, "y": { "scale": "yscale", "field": "amount" }, "y2": { "scale": "yscale", "value": 0 } }, "update": { "fill": { "value": "steelblue" } }, "hover": { "fill": { "value": "red" } } } }, { "type": "text", "encode": { "enter": { "align": { "value": "center" }, "baseline": { "value": "bottom" }, "fill": { "value": "#333" } }, "update": { "x": { "scale": "xscale", "signal": "tooltip.category", "band": 0.5 }, "y": { "scale": "yscale", "signal": "tooltip.amount", "offset": -2 }, "text": { "signal": "tooltip.amount" }, "fillOpacity": [{ "test": "datum === tooltip", "value": 0 }, { "value": 1 }] } } }]
};
const opt = {};
const type = "vega";
const id = "2a522180-bd9f-476c-b4f0-5e6311bccbc3";
const output_area = this;
require(["nbextensions/jupyter-vega/index"], function (vega) {
const target = document.createElement("div");
target.id = id;
target.className = "vega-embed";
const style = document.createElement("style");
style.textContent = [".vega-embed .error p {", " color: firebrick;", " font-size: 14px;", "}", ].join("\\\\n");
// element is a jQuery wrapped DOM element inside the output area
// see http://ipython.readthedocs.io/en/stable/api/generated/\\
// IPython.display.html#IPython.display.Javascript.__init__
element[0].appendChild(target);
element[0].appendChild(style);
vega.render("#" + id, spec, type, opt, output_area);
});
As you can see above, the output takes following assumptions:
- AMD loader knows where to fetch
nbextensions/jupyter-vega/index
element
is a jQuery wrapped DOM element inside the output areathis
contains output information
If VS Code core insert this script directly into the DOM tree, it won't execute successfully as the core is not aware of above assumptions. While the notebook provider can wrap it with all required information:
require.config({
paths: {
"nbextensions/jupyter-vega/index": "vscode-resource://file///Users/penlv/code/vscode/extensions/notebook-test/dist/jupyter-vega/index.js"
}
});
(function(element) {
const spec =
...
const output_area = this;
require(["nbextensions/jupyter-vega/index"], function (vega) {
....
}
}).call({ outputs: ["#vegatest"] }, [document.getElementById("vegatest")]);