Notebook ipywidgets output

This commit is contained in:
rebornix
2020-01-09 14:16:07 -08:00
parent 50aaa6c4ff
commit aaa34d4968
14 changed files with 1874 additions and 66 deletions
@@ -9,12 +9,28 @@
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
resolve: {
mainFields: ['module', 'main']
},
entry: {
extension: './src/extension.ts',
module.exports = [
withDefaults({
context: __dirname,
resolve: {
mainFields: ['module', 'main']
},
entry: {
extension: './src/extension.ts',
}
}),
{
mode: 'development',
entry: './output/ipywidgets.js',
output: {
filename: 'ipywidgets.js',
publicPath: 'dist/'
},
target: 'web',
module: {
rules: [
{ test: /\.css$/, loader: "style-loader!css-loader" }
]
},
}
});
]
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var WidgetManager = require("./manager").WidgetManager;
require('@jupyter-widgets/controls/css/widgets.css');
var widgetarea = document.getElementsByClassName("widgetarea")[0];
manager = new WidgetManager(widgetarea);
window.manager = manager;
manager.renderTag = () => {
let tags = widgetarea.querySelectorAll('script[type="application/vnd.jupyter.widget-state+json"]');
if (tags.length) {
let state = JSON.parse(tags[0].innerHTML);
window.manager.set_state(state).then(function (models) {
window.models = models;
}).then(function () {
let tags = widgetarea.querySelectorAll('script[type="application/vnd.jupyter.widget-view+json"]');
for (let i = 0; i != tags.length; ++i) {
let viewtag = tags[i];
let widgetViewObject = JSON.parse(viewtag.innerHTML);
let model_id = widgetViewObject.model_id;
let model = window.models.filter((item) => {
return item.model_id == model_id;
})[0];
if (model !== undefined) {
let widgetTag = document.createElement('div');
widgetTag.className = 'widget-subarea';
viewtag.parentElement.insertBefore(widgetTag, viewtag);
viewtag.parentElement.removeChild(viewtag);
manager.display_model(undefined, model, { el: widgetTag });
}
}
})
}
}
manager.renderTag();
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var base = require('@jupyter-widgets/base');
var controls = require('@jupyter-widgets/controls');
var LuminoWidget = require('@lumino/widgets').Widget;
class WidgetManager extends base.ManagerBase {
constructor(el) {
super();
this.el = el;
}
loadClass(className, moduleName, moduleVersion) {
return new Promise(function(resolve, reject) {
if (moduleName === '@jupyter-widgets/controls') {
resolve(controls);
} else if (moduleName === '@jupyter-widgets/base') {
resolve(base)
} else {
var fallback = function(err) {
let failedId = err.requireModules && err.requireModules[0];
if (failedId) {
console.log(`Falling back to unpkg.com for ${moduleName}@${moduleVersion}`);
window.require([`https://unpkg.com/${moduleName}@${moduleVersion}/dist/index.js`], resolve, reject);
} else {
throw err;
}
};
window.require([`${moduleName}.js`], resolve, fallback);
}
}).then(function(module) {
if (module[className]) {
return module[className];
} else {
return Promise.reject(`Class ${className} not found in module ${moduleName}@${moduleVersion}`);
}
});
}
display_view(msg, view, options) {
var that = this;
return Promise.resolve(view).then(function(view) {
let el = options.el || that.el;
LuminoWidget.attach(view.pWidget, el);
view.on('remove', function() {
console.log('View removed', view);
});
return view;
});
};
_get_comm_info() {
return Promise.resolve({});
};
_create_comm() {
return Promise.reject('no comms available');
}
}
exports.WidgetManager = WidgetManager;
+11 -3
View File
@@ -14,7 +14,7 @@
"engines": {
"vscode": "^1.40.0"
},
"main": "./out/extension",
"main": "./dist/extension",
"categories": [
"Other"
],
@@ -39,8 +39,16 @@
"watch": "tsc -watch -p ./"
},
"dependencies": {
"@jupyter-widgets/base": "^2.0.2",
"@jupyter-widgets/controls": "^1.5.3",
"@lumino/widgets": "^1.3.0",
"typescript": "^3.6.4",
"vscode-extension-telemetry": "0.1.1",
"vscode-nls": "^4.0.0",
"typescript": "^3.6.4"
"vscode-nls": "^4.0.0"
},
"devDependencies": {
"webpack-cli": "^3.3.10",
"css-loader": "^3.4.1",
"style-loader": "^1.1.2"
}
}
+1 -1
View File
@@ -9,6 +9,6 @@ import { NotebookProvider } from './notebookProvider';
export function activate(context: vscode.ExtensionContext) {
console.log(context.extensionPath);
context.subscriptions.push(vscode.window.registerNotebookProvider('jupyter', new NotebookProvider()));
context.subscriptions.push(vscode.window.registerNotebookProvider('jupyter', new NotebookProvider(context.extensionPath)));
}
@@ -4,18 +4,29 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
export class Cell implements vscode.ICell {
public outputs: any[] = [];
public outputs: vscode.IOutput[] = [];
constructor(
public source: string[],
public cell_type: 'markdown' | 'code',
private _outputs: any[]
private _outputs: vscode.IOutput[]
) {
}
containHTML() {
return this._outputs && this._outputs.some(output => {
if (output.output_type === 'display_data' && output.data['text/html']) {
return true;
}
return false;
});
}
fillInOutputs() {
this.outputs = this._outputs;
}
@@ -35,7 +46,7 @@ export class NotebookProvider implements vscode.NotebookProvider {
onDidChangeNotebook: vscode.Event<{ resource: vscode.Uri; notebook: vscode.INotebook; }> = this._onDidChangeNotebook.event;
private _notebooks: Map<string, JupyterNotebook> = new Map();
constructor() {
constructor(private _extensionPath: string) {
}
async resolveNotebook(resource: vscode.Uri): Promise<vscode.INotebook | undefined> {
@@ -68,7 +79,35 @@ export class NotebookProvider implements vscode.NotebookProvider {
async executeNotebook(resource: vscode.Uri): Promise<void> {
if (this._notebooks.has(resource.fsPath)) {
let notebook = this._notebooks.get(resource.fsPath);
notebook!.cells.forEach(cell => cell.fillInOutputs());
let preloadScript = false;
notebook!.cells.forEach(cell => {
if (!preloadScript) {
let containHTML = cell.containHTML();
if (containHTML) {
preloadScript = true;
cell.fillInOutputs();
const scriptPathOnDisk = vscode.Uri.file(
path.join(this._extensionPath, 'dist', 'ipywidgets.js')
);
let scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' });
cell.outputs.unshift(
{
'output_type': 'display_data',
'data': {
'text/html': [
`<script src="${scriptUri}"></script>\n`,
]
}
}
);
} else {
cell.fillInOutputs();
}
} else {
cell.fillInOutputs();
}
});
this._onDidChangeNotebook.fire({ resource, notebook: notebook! });
}
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -560,9 +560,9 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
this.rowsContainer.style.left = `-${renderLeft}px`;
console.log('------');
const date = new Date();
console.log('list view render top change, old ', this.rowsContainer.style.top, ' new ', `-${renderTop}` + ' - ' + date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
// console.log('------');
// const date = new Date();
// console.log('list view render top change, old ', this.rowsContainer.style.top, ' new ', `-${renderTop}` + ' - ' + date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
this.rowsContainer.style.top = `-${renderTop}px`;
if (this.horizontalScrolling) {
+2 -6
View File
@@ -1302,14 +1302,10 @@ declare module 'vscode' {
export interface IDisplayOutput {
output_type: 'display_data';
data: { string: string };
data: { [key: string]: any };
}
export interface IGenericOutput {
output_type: string;
}
export type IOutput = IStreamOutput | any;
export type IOutput = IStreamOutput | IErrorOutput | IDisplayOutput;
export interface ICell {
source: string[];
@@ -85,7 +85,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
$registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): void {
let controller = new MainThreadNotebookController(this);
this._notebookProviders.set(viewType, controller);
this._notebookService.registerNotebookController(viewType, controller);
this._notebookService.registerNotebookController(viewType, extension, controller);
}
$unregisterNotebookProvider(viewType: string): void {
@@ -185,7 +185,7 @@ export interface NotebookHandler {
editNotebookCell(cell: ViewCell): void;
saveNotebookCell(cell: ViewCell): void;
layoutElement(cell: ViewCell, height: number): void;
createContentWidget(cell: ViewCell, shadowContent: string, offset: number): void;
createContentWidget(cell: ViewCell, index: number, shadowContent: string, offset: number): void;
disposeViewCell(cell: ViewCell): void;
triggerWheel(event: IMouseWheelEvent): void;
}
@@ -581,7 +581,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
templateData.outputContainer?.appendChild(result.element);
if (result.shadowContent) {
hasDynamicHeight = false;
this.handler.createContentWidget(element, result.shadowContent, totalHeight + 8);
this.handler.createContentWidget(element, i, result.shadowContent, totalHeight + 8);
}
}
}
@@ -634,7 +634,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
templateData.outputContainer?.appendChild(result.element);
if (result.shadowContent) {
hasDynamicHeight = false;
this.handler.createContentWidget(element, result.shadowContent, totalHeight + 8);
this.handler.createContentWidget(element, i, result.shadowContent, totalHeight + 8);
}
}
}
@@ -8,6 +8,7 @@ import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/br
import * as DOM from 'vs/base/browser/dom';
import * as UUID from 'vs/base/common/uuid';
import { Disposable } from 'vs/base/common/lifecycle';
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
export interface IDimentionMessage {
@@ -61,8 +62,9 @@ export class BackLayerWebView extends Disposable {
public element: HTMLElement;
public webview: WebviewElement;
public mapping: Map<string, { cell: ViewCell, offset: number, top: number }> = new Map();
public outputMapping: Map<string, boolean> = new Map();
constructor(public webviewService: IWebviewService, public notebookHandler: NotebookHandler) {
constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookHandler: NotebookHandler) {
super();
this.element = document.createElement('div');
this.element.style.width = 'calc(100% - 36px)';
@@ -70,19 +72,44 @@ export class BackLayerWebView extends Disposable {
this.element.style.position = 'absolute';
this.element.style.margin = '0px 0 0px 24px';
let content = /* html */`
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body style="overflow: hidden;">
<div id='container' style="position: absolute;width:100%;"></div>
<div id='container' class="widgetarea" style="position: absolute;width:100%;"></div>
<script>
(function () {
// eslint-disable-next-line no-undef
const vscode = acquireVsCodeApi();
console.log('a');
const preservedScriptAttributes = {
type: true,
src: true,
nonce: true,
noModule: true,
async: true
};
// derived from https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/core/DOMEval.js
const domEval = (container) => {
var arr = Array.from(container.getElementsByTagName('script'));
for (let n = 0; n < arr.length; n++) {
let node = arr[n];
let scriptTag = document.createElement('script');
scriptTag.text = node.innerText;
for (let key in preservedScriptAttributes ) {
const val = node[key] || node.getAttribute && node.getAttribute(key);
if (val) {
scriptTag.setAttribute(key, val);
}
}
// TODO: should script with src not be removed?
container.appendChild(scriptTag).parentNode.removeChild(scriptTag);
}
};
window.addEventListener('message', event => {
let id = event.data.id;
@@ -90,30 +117,39 @@ export class BackLayerWebView extends Disposable {
switch (event.data.type) {
case 'html':
{
let content = event.data.content;
let newElement = document.createElement('div');
newElement.style.position = 'absolute';
newElement.style.top = event.data.top + 'px';
newElement.id = id;
document.getElementById('container').appendChild(newElement);
newElement.innerHTML = content;
var arr = newElement.getElementsByTagName('script');
for (let n = 0; n < arr.length; n++) {
eval(arr[n].innerHTML); //run script inside div
let cellOutputContainer = document.getElementById(id);
if (!cellOutputContainer) {
let newElement = document.createElement('div');
newElement.style.position = 'absolute';
newElement.style.top = event.data.top + 'px';
newElement.id = id;
document.getElementById('container').appendChild(newElement);
cellOutputContainer = newElement;
}
vscode.postMessage({
type: 'dimension',
id: id,
data: {
height: newElement.clientHeight
}
});
let outputNode = document.createElement('div');
let content = event.data.content;
outputNode.innerHTML = content;
cellOutputContainer.appendChild(outputNode);
// eval
domEval(outputNode);
setTimeout(() => {
vscode.postMessage({
type: 'dimension',
id: id,
data: {
height: cellOutputContainer.clientHeight
}
});
}, 200);
}
break;
case 'view-scroll':
{
const date = new Date();
console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
// console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
document.getElementById('container').style.top = event.data.top + 'px';
for (let i = 0; i < event.data.widgets.length; i++) {
@@ -159,9 +195,9 @@ export class BackLayerWebView extends Disposable {
this.notebookHandler.layoutElement(cell, totalHeight + 32 + height);
}
} else if (data.type === 'scroll-ack') {
const date = new Date();
const top = data.data.top;
console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
// const date = new Date();
// const top = data.data.top;
// console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
}
}));
}
@@ -170,7 +206,8 @@ export class BackLayerWebView extends Disposable {
const webview = webviewService.createWebview('' + UUID.generateUuid(), {
enableFindWidget: false,
}, {
allowScripts: true
allowScripts: true,
localResourceRoots: this.notebookService.getNotebookProviderResourceRoots()
});
webview.html = content;
return webview;
@@ -24,6 +24,7 @@ import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/contentWidget';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
const $ = DOM.$;
@@ -46,7 +47,8 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler {
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IStorageService storageService: IStorageService,
@IWebviewService private webviewService: IWebviewService
@IWebviewService private webviewService: IWebviewService,
@INotebookService private notebookService: INotebookService
) {
super(NotebookEditor.ID, telemetryService, themeService, storageService);
}
@@ -116,7 +118,7 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler {
}
);
this.webview = new BackLayerWebView(this.webviewService, this);
this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this);
this.list.view.rowsContainer.appendChild(this.webview.element);
this._register(this.list);
}
@@ -125,7 +127,7 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler {
this.list?.triggerScrollFromMouseWheelEvent(event);
}
createContentWidget(cell: ViewCell, shadowContent: string, offset: number) {
createContentWidget(cell: ViewCell, outputIndex: number, shadowContent: string, offset: number) {
if (!this.webview) {
return;
}
@@ -134,6 +136,12 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler {
let index = this.model!.getNotebook().cells.indexOf(cell.cell);
let top = this.list?.getElementTop(index) || 0;
this.webview!.createContentWidget(cell, offset, shadowContent, top + offset);
this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true);
} else if (!this.webview!.outputMapping.has(cell.id + `-${outputIndex}`)) {
let index = this.model!.getNotebook().cells.indexOf(cell.cell);
let top = this.list?.getElementTop(index) || 0;
this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true);
this.webview!.createContentWidget(cell, offset, shadowContent, top + offset);
} else {
let index = this.model!.getNotebook().cells.indexOf(cell.cell);
let top = this.list?.getElementTop(index) || 0;
@@ -185,9 +193,8 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler {
if (this.webview) {
this.webview?.clearContentWidgets();
} else {
this.webview = new BackLayerWebView(this.webviewService, this);
this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this);
this.list?.view.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element);
}
this.model = model;
@@ -9,6 +9,7 @@ import { INotebook } from 'vs/editor/common/modes';
import { URI } from 'vs/base/common/uri';
import { notebookExtensionPoint } 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';
export const INotebookService = createDecorator<INotebookService>('notebookService');
@@ -20,11 +21,12 @@ export interface IMainNotebookController {
export interface INotebookService {
_serviceBrand: undefined;
registerNotebookController(viewType: string, controller: IMainNotebookController): void;
registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void;
unregisterNotebookProvider(viewType: string): void;
resolveNotebook(viewType: string, uri: URI): Promise<INotebook | undefined>;
executeNotebook(viewType: string, uri: URI): Promise<void>;
getContributedNotebook(resource: URI): readonly NotebookProviderInfo[];
getNotebookProviderResourceRoots(): URI[];
}
export class NotebookInfoStore {
@@ -55,7 +57,7 @@ export class NotebookInfoStore {
export class NotebookService extends Disposable implements INotebookService {
_serviceBrand: undefined;
private readonly _notebookProviders = new Map<string, IMainNotebookController>();
private readonly _notebookProviders = new Map<string, { controller: IMainNotebookController, extensionData: NotebookExtensionDescription }>();
public notebookProviderInfoStore: NotebookInfoStore = new NotebookInfoStore();
constructor() {
@@ -78,8 +80,8 @@ export class NotebookService extends Disposable implements INotebookService {
}
registerNotebookController(viewType: string, controller: IMainNotebookController) {
this._notebookProviders.set(viewType, controller);
registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) {
this._notebookProviders.set(viewType, { extensionData, controller });
}
unregisterNotebookProvider(viewType: string): void {
@@ -90,7 +92,7 @@ export class NotebookService extends Disposable implements INotebookService {
let provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.resolveNotebook(viewType, uri);
return provider.controller.resolveNotebook(viewType, uri);
}
return Promise.resolve(undefined);
@@ -100,7 +102,7 @@ export class NotebookService extends Disposable implements INotebookService {
let provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.executeNotebook(viewType, uri);
return provider.controller.executeNotebook(viewType, uri);
}
return;
@@ -109,4 +111,13 @@ export class NotebookService extends Disposable implements INotebookService {
public getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] {
return this.notebookProviderInfoStore.getContributedNotebook(resource);
}
public getNotebookProviderResourceRoots(): URI[] {
let ret: URI[] = [];
this._notebookProviders.forEach(val => {
ret.push(URI.revive(val.extensionData.location));
});
return ret;
}
}