зеркало из https://github.com/microsoft/gather.git
Support persistent history for multiple notebooks in Jupyter Lab
This commit is contained in:
Родитель
189b3c937c
Коммит
d47c210736
|
@ -4,6 +4,7 @@ package-lock.json
|
|||
# Dependencies
|
||||
node_modules/
|
||||
*.egg-info/
|
||||
venv/
|
||||
|
||||
# Generated parsers
|
||||
python3.js
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "livecells",
|
||||
"version": "0.1.0",
|
||||
"name": "gathering",
|
||||
"version": "0.2.0",
|
||||
"description": "Add live programming to Jupyter cells",
|
||||
"author": "",
|
||||
"main": "lib/lab/index.js",
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import { INotebookModel } from "@jupyterlab/notebook";
|
||||
import { JSONArray, JSONExt, JSONObject } from "@phosphor/coreutils";
|
||||
import { log } from "util";
|
||||
import { SimpleCell } from "../packages/cell";
|
||||
import { CellExecution } from "../slicing/ExecutionSlicer";
|
||||
import { GatherModel } from "../packages/gather";
|
||||
|
||||
/**
|
||||
* Key for accessing execution history in Jupyter notebook metadata.
|
||||
*/
|
||||
export const EXECUTION_HISTORY_METADATA_KEY = "history";
|
||||
|
||||
/**
|
||||
* Load history for a Jupyter notebook from a notebook's metadata. This method is asynchronous
|
||||
* because it needs to continually poll the notebook's metadata when Lab first loads.
|
||||
*/
|
||||
export function loadHistory(notebookModel: INotebookModel, gatherModel: GatherModel) {
|
||||
if (_notebookHistoryMetadataFound(notebookModel)) {
|
||||
_tryLoadHistory(notebookModel, gatherModel);
|
||||
return;
|
||||
}
|
||||
log("No history found in notebook metadata.");
|
||||
}
|
||||
|
||||
function _notebookHistoryMetadataFound(notebookModel: INotebookModel): boolean {
|
||||
return notebookModel.metadata.has(EXECUTION_HISTORY_METADATA_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if the format of the execution log metadata is unrecognized.
|
||||
*/
|
||||
function _tryLoadHistory(notebookModel: INotebookModel, gatherModel: GatherModel) {
|
||||
|
||||
let historyCells = notebookModel.metadata.get(EXECUTION_HISTORY_METADATA_KEY);
|
||||
if (!JSONExt.isArray(historyCells)) {
|
||||
log("Unexpected history metadata format: no array found");
|
||||
return;
|
||||
}
|
||||
|
||||
let executionsArray = historyCells as JSONArray;
|
||||
for (let executionValue of executionsArray) {
|
||||
if (!JSONExt.isObject(executionValue)) {
|
||||
log("Unexpected history metadata format: cell execution is not an object");
|
||||
return;
|
||||
}
|
||||
let executionJsonObject = executionValue as JSONObject;
|
||||
let cellExecution = _loadExecutionFromJson(executionJsonObject);
|
||||
if (cellExecution == null) {
|
||||
log("Unexpected cell execution format. Loading history aborted.");
|
||||
return;
|
||||
}
|
||||
gatherModel.executionLog.addExecutionToLog(cellExecution);
|
||||
gatherModel.lastExecutedCell = cellExecution.cell;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if the format of the cell execution JSON is unrecognized.
|
||||
*/
|
||||
function _loadExecutionFromJson(executionJson: JSONObject): CellExecution {
|
||||
|
||||
function _getString(json: JSONObject, key: string): string {
|
||||
if (!json.hasOwnProperty(key) || typeof json[key] != 'string') {
|
||||
log("Could not find key " + key + "in object " + json);
|
||||
return null;
|
||||
}
|
||||
return json[key] as string;
|
||||
}
|
||||
|
||||
function _getNumber(json: JSONObject, key: string): number {
|
||||
if (!json.hasOwnProperty(key) || typeof json[key] != 'number') {
|
||||
log("Could not find key " + key + "in object " + json);
|
||||
return null;
|
||||
}
|
||||
return json[key] as number;
|
||||
}
|
||||
|
||||
function _getBoolean(json: JSONObject, key: string): boolean {
|
||||
if (!json.hasOwnProperty(key) || typeof json[key] != 'boolean') {
|
||||
log("Could not find key " + key + "in object " + json);
|
||||
return null;
|
||||
}
|
||||
return json[key] as boolean;
|
||||
}
|
||||
|
||||
if (!executionJson.hasOwnProperty('cell') || !JSONExt.isObject(executionJson['cell'])) {
|
||||
log("Unexpected cell data format: cell is not an object");
|
||||
return null;
|
||||
}
|
||||
let cellJson = executionJson['cell'] as JSONObject;
|
||||
|
||||
let id = _getString(cellJson, 'id');
|
||||
let persistentId = _getString(cellJson, 'persistentId');
|
||||
let executionCount = _getNumber(cellJson, 'executionCount');
|
||||
let hasError = _getBoolean(cellJson, 'hasError');
|
||||
let isCode = _getBoolean(cellJson, 'isCode');
|
||||
let text = _getString(cellJson, 'text');
|
||||
|
||||
let executionTimeString = _getString(executionJson, "executionTime");
|
||||
let executionTime = new Date(executionTimeString);
|
||||
|
||||
if (id == null || executionCount == null || hasError == null ||
|
||||
isCode == null || text == null || executionTime == null) {
|
||||
log("Cell could not be loaded, as it's missing a critical field.");
|
||||
return null;
|
||||
}
|
||||
|
||||
let cell = new SimpleCell(id, persistentId, executionCount, hasError, isCode, text);
|
||||
return new CellExecution(cell, executionTime);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { ExecutionLogSlicer } from "../slicing/ExecutionSlicer";
|
||||
import { INotebookModel } from "@jupyterlab/notebook";
|
||||
import { EXECUTION_HISTORY_METADATA_KEY } from "./load";
|
||||
import { JSONObject, JSONArray } from "@phosphor/coreutils";
|
||||
|
||||
interface CellExecutionJson extends JSONObject {
|
||||
executionTime: string;
|
||||
cell: CellJson;
|
||||
}
|
||||
|
||||
interface CellJson extends JSONObject {
|
||||
id: string;
|
||||
persistentId: string;
|
||||
executionCount: number;
|
||||
hasError: boolean;
|
||||
isCode: boolean;
|
||||
text: string;
|
||||
gathered: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is complementary with the loadHistory method. Make sure that any chances to the
|
||||
* format of stored history is reflected in changes to that method.
|
||||
*/
|
||||
export function storeHistory(notebookModel: INotebookModel, executionLog: ExecutionLogSlicer) {
|
||||
|
||||
let cellExecutionsJson: JSONArray = [];
|
||||
|
||||
for (let cellExecution of executionLog.cellExecutions) {
|
||||
|
||||
let cell = cellExecution.cell;
|
||||
let cellJson = new Object(null) as CellJson;
|
||||
cellJson.id = cell.id;
|
||||
cellJson.persistentId = cell.persistentId;
|
||||
cellJson.executionCount = cell.executionCount;
|
||||
cellJson.hasError = cell.hasError;
|
||||
cellJson.isCode = cell.isCode;
|
||||
cellJson.text = cell.text;
|
||||
|
||||
let cellExecutionJson = new Object(null) as CellExecutionJson;
|
||||
cellExecutionJson.cell = cellJson;
|
||||
cellExecutionJson.executionTime = cellExecution.executionTime.toISOString();
|
||||
|
||||
cellExecutionsJson.push(cellExecutionJson);
|
||||
}
|
||||
|
||||
notebookModel.metadata.set(EXECUTION_HISTORY_METADATA_KEY, cellExecutionsJson);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import {
|
||||
Widget
|
||||
} from '@phosphor/widgets';
|
||||
|
||||
import {
|
||||
Notebook
|
||||
} from '@jupyterlab/notebook';
|
||||
|
||||
|
||||
|
||||
const TOOLBAR_CHECKBOX_CLASS = 'jp-Notebook-toolbarCheckbox';
|
||||
|
||||
|
||||
export
|
||||
class ToolbarCheckbox extends Widget {
|
||||
|
||||
private input: HTMLInputElement;
|
||||
|
||||
constructor(widget: Notebook) {
|
||||
let label = document.createElement('label');
|
||||
label.innerText = 'Live code';
|
||||
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('type', 'checkbox');
|
||||
input.className = TOOLBAR_CHECKBOX_CLASS;
|
||||
label.appendChild(input);
|
||||
|
||||
super({ node: label });
|
||||
this.input = input;
|
||||
this.addClass(TOOLBAR_CHECKBOX_CLASS);
|
||||
}
|
||||
|
||||
public get checked() {
|
||||
return this.input.checked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { NotebookPanel } from "@jupyterlab/notebook";
|
||||
import { Cell, CodeCell, isCodeCellModel } from "@jupyterlab/cells";
|
||||
import { ICell } from "../packages/cell";
|
||||
import CodeMirror from "codemirror";
|
||||
import { CodeMirrorEditor } from "@jupyterlab/codemirror";
|
||||
import { LabCell } from "./LabCell";
|
||||
|
||||
/**
|
||||
* Finds the HTML elements in a notebook corresponding to a cell. Useful for looking up HTML
|
||||
* elements when all you have is a copy of a notebook cell and not the actual cell.
|
||||
*/
|
||||
export class NotebookElementFinder {
|
||||
|
||||
constructor(notebook: NotebookPanel) {
|
||||
this._notebook = notebook;
|
||||
}
|
||||
|
||||
getCellWithPersistentId(persistentId: string): Cell | null {
|
||||
for (let cell of this._notebook.content.widgets) {
|
||||
if (isCodeCellModel(cell.model)) {
|
||||
let labCell = new LabCell(cell.model);
|
||||
if (labCell.persistentId == persistentId) {
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cell from the notebook.
|
||||
* (Don't call this right after a cell execution event, as it takes a while for the
|
||||
* execution count to update in an executed cell).
|
||||
*/
|
||||
getCell(persistentId: string, executionCount?: number): Cell | null {
|
||||
let cell = this.getCellWithPersistentId(persistentId);
|
||||
if (cell != null && (cell as CodeCell).model.executionCount == executionCount) {
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the element for the code editor for a cell.
|
||||
*/
|
||||
getEditor(cell: ICell): CodeMirror.Editor | null {
|
||||
let widget = this.getCellWithPersistentId(cell.persistentId);
|
||||
return this._getEditor(widget);
|
||||
}
|
||||
|
||||
getEditorWithExecutionCount(cell: ICell): CodeMirror.Editor | null {
|
||||
let widget = this.getCell(cell.persistentId, cell.executionCount);
|
||||
return this._getEditor(widget);
|
||||
}
|
||||
|
||||
_getEditor(cell: Cell): CodeMirror.Editor | null {
|
||||
if (cell && cell.editor instanceof CodeMirrorEditor) {
|
||||
return cell.editor.editor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds HTML elements for cell outputs in a notebook.
|
||||
*/
|
||||
getOutputs(cell: ICell): HTMLElement[] {
|
||||
let cellWidget = this.getCell(cell.persistentId, cell.executionCount);
|
||||
let outputElements: HTMLElement[] = [];
|
||||
if (cellWidget == null) {
|
||||
return outputElements;
|
||||
}
|
||||
let cellElement = cellWidget.node;
|
||||
var outputNodes = cellElement.querySelectorAll(".jp-OutputArea-output");
|
||||
for (var i = 0; i < outputNodes.length; i++) {
|
||||
if (outputNodes[i] instanceof HTMLElement) {
|
||||
outputElements.push(outputNodes[i] as HTMLElement);
|
||||
}
|
||||
}
|
||||
return outputElements;
|
||||
}
|
||||
|
||||
private _notebook: NotebookPanel;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { GatherModel } from "../packages/gather";
|
||||
import { IObservableList } from "@jupyterlab/observables";
|
||||
import { ICellModel, CodeCellModel } from "@jupyterlab/cells";
|
||||
import { LabCell, copyICodeCellModel } from "./LabCell";
|
||||
import { NotebookPanel } from "@jupyterlab/notebook";
|
||||
|
||||
export class ExecutionLogger {
|
||||
|
||||
constructor(notebook: NotebookPanel, gatherModel: GatherModel) {
|
||||
this._gatherModel = gatherModel;
|
||||
let existingCells = notebook.content.model.cells;
|
||||
for (let i = 0; i < existingCells.length; i++) {
|
||||
this._listenForCellExecution(existingCells.get(i));
|
||||
}
|
||||
this._listenToFutureAddedCells(notebook);
|
||||
}
|
||||
|
||||
_listenForCellExecution(cellModel: ICellModel) {
|
||||
// When a cell is added, register for its state changes.
|
||||
if (cellModel.type !== 'code') { return; }
|
||||
cellModel.stateChanged.connect((changedCell, cellStateChange) => {
|
||||
if (changedCell instanceof CodeCellModel && cellStateChange.name === "executionCount" && cellStateChange.newValue !== undefined && cellStateChange.newValue !== null) {
|
||||
let cellClone = copyICodeCellModel(changedCell);
|
||||
const cell = new LabCell(cellClone);
|
||||
this._gatherModel.executionLog.logExecution(cell);
|
||||
this._gatherModel.lastExecutedCell = cell;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_listenToFutureAddedCells(notebook: NotebookPanel) {
|
||||
notebook.content.model.cells.changed.connect(
|
||||
(_, change) => this._onCellsChanged(change));
|
||||
}
|
||||
|
||||
_onCellsChanged(cellListChange: IObservableList.IChangedArgs<ICellModel>): void {
|
||||
if (cellListChange.type === 'add') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
this._listenForCellExecution(cellModel);
|
||||
}
|
||||
}
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
import { Clipboard as JupyterClipboard } from "@jupyterlab/apputils";
|
||||
import { nbformat } from "@jupyterlab/coreutils";
|
||||
import { DocumentManager } from "@jupyterlab/docmanager";
|
||||
import { IDocumentWidget } from "@jupyterlab/docregistry";
|
||||
import { INotebookTracker, NotebookPanel } from "@jupyterlab/notebook";
|
||||
import { Kernel } from "@jupyterlab/services";
|
||||
import { OutputSelection } from "../packages/gather";
|
||||
import { SlicedExecution } from "../slicing/ExecutionSlicer";
|
||||
import { FileEditor } from "@jupyterlab/fileeditor";
|
||||
import { ISignal, Signal } from "@phosphor/signaling";
|
||||
import { JSONObject } from "@phosphor/coreutils";
|
||||
|
||||
/**
|
||||
* Listens to changes to the clipboard.
|
||||
*/
|
||||
export interface IClipboardListener {
|
||||
/**
|
||||
* Called when something is copied to the clipboard.
|
||||
*/
|
||||
onCopy: (slice: SlicedExecution, clipboard: Clipboard) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather code to the clipboard. Base code found in
|
||||
* packages/notebooks/src/actions.tsx in Jupyter Lab project.
|
||||
*/
|
||||
export class Clipboard {
|
||||
|
||||
static getInstance(): Clipboard {
|
||||
return this.INSTANCE;
|
||||
}
|
||||
|
||||
get copied(): ISignal<this, SlicedExecution> {
|
||||
return this._copied;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(andrewhead): selected outputs should be passed as arguments to this function too.
|
||||
*/
|
||||
copy(slice: SlicedExecution) {
|
||||
const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
|
||||
if (slice) {
|
||||
// let cellJson = sliceToCellJson(slice, this._gatherModel.selectedOutputs.concat());
|
||||
let cellJson = getCellsJsonForSlice(slice, []);
|
||||
const clipboard = JupyterClipboard.getInstance();
|
||||
clipboard.clear();
|
||||
clipboard.setData(JUPYTER_CELL_MIME, cellJson);
|
||||
this._copied.emit(slice);
|
||||
}
|
||||
}
|
||||
|
||||
private static INSTANCE = new Clipboard();
|
||||
private _copied = new Signal<this, SlicedExecution>(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partial spec for a kernel that will allow us to launch another kernel for the same language.
|
||||
*/
|
||||
function _createKernelSpecForCurrentWidget(notebooks: INotebookTracker): Partial<Kernel.IModel> {
|
||||
let spec = new Object(null) as JSONObject;
|
||||
if (notebooks.currentWidget && notebooks.currentWidget.session.kernel) {
|
||||
spec.name = notebooks.currentWidget.session.kernel.model.name;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new scripts containing program slices.
|
||||
*/
|
||||
export class ScriptOpener {
|
||||
|
||||
constructor(documentManager: DocumentManager, notebooks: INotebookTracker) {
|
||||
this._documentManager = documentManager;
|
||||
this._notebooks = notebooks;
|
||||
}
|
||||
|
||||
openScriptForSlice(slice: SlicedExecution) {
|
||||
/*
|
||||
* TODO(andrewhead): give the document a context-sensitive name, say the name of the result.
|
||||
*/
|
||||
this._documentManager.newUntitled({ ext: 'py' }).then(model => {
|
||||
let kernelSpec = _createKernelSpecForCurrentWidget(this._notebooks);
|
||||
let editor = this._documentManager.open(model.path, undefined, kernelSpec) as IDocumentWidget<FileEditor>;
|
||||
editor.context.ready.then(() => {
|
||||
if (slice) {
|
||||
let cellsJson = getCellsJsonForSlice(slice, []);
|
||||
let scriptText = cellsJson.map(cellJson => cellJson.source).join("\n");
|
||||
editor.content.model.value.text = scriptText;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _documentManager: DocumentManager;
|
||||
private _notebooks: INotebookTracker;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens new notebooks containing program slices.
|
||||
*/
|
||||
export class NotebookOpener {
|
||||
|
||||
constructor(documentManager: DocumentManager, notebooks: INotebookTracker) {
|
||||
this._documentManager = documentManager;
|
||||
this._notebooks = notebooks;
|
||||
}
|
||||
|
||||
openNotebookForSlice(slice: SlicedExecution) {
|
||||
/*
|
||||
* TODO(andrewhead): give the document a context-sensitive name, say the name of the result.
|
||||
*/
|
||||
this._documentManager.newUntitled({ ext: 'ipynb' }).then(model => {
|
||||
let kernelSpec = _createKernelSpecForCurrentWidget(this._notebooks);
|
||||
const widget = this._documentManager.open(model.path, undefined, kernelSpec) as NotebookPanel;
|
||||
widget.context.ready.then(() => {
|
||||
const notebookModel = widget.content.model;
|
||||
let notebookJson = notebookModel.toJSON() as nbformat.INotebookContent;
|
||||
notebookJson.cells = []
|
||||
if (slice) {
|
||||
let cellsJson = getCellsJsonForSlice(slice, []);
|
||||
for (let cell of cellsJson) {
|
||||
notebookJson.cells.push(cell);
|
||||
}
|
||||
}
|
||||
notebookModel.fromJSON(notebookJson);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _documentManager: DocumentManager;
|
||||
private _notebooks: INotebookTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert program slice to list of cell JSONs
|
||||
*/
|
||||
function getCellsJsonForSlice(slice: SlicedExecution, outputSelections?: OutputSelection[]): nbformat.ICodeCell[] {
|
||||
|
||||
const SHOULD_SLICE_CELLS = true;
|
||||
|
||||
outputSelections = outputSelections || [];
|
||||
|
||||
return slice.cellSlices
|
||||
.map((cellSlice) => {
|
||||
let slicedCell = cellSlice.cell;
|
||||
if (SHOULD_SLICE_CELLS) {
|
||||
slicedCell = slicedCell.copy();
|
||||
slicedCell.text = cellSlice.textSliceLines;
|
||||
}
|
||||
let cellJson = slicedCell.toJupyterJSON();
|
||||
// This new cell hasn't been executed yet. So don't mark it as having been executed.
|
||||
cellJson.execution_count = null;
|
||||
// Add a flag to distinguish gathered cells from other cells.
|
||||
if (!cellJson.hasOwnProperty("metadata")) {
|
||||
cellJson.metadata = {};
|
||||
}
|
||||
cellJson.metadata.gathered = true;
|
||||
// Filter to just those outputs that were selected.
|
||||
let originalOutputs = cellJson.outputs;
|
||||
cellJson.outputs = [];
|
||||
if (originalOutputs) {
|
||||
for (let i = 0; i < originalOutputs.length; i++) {
|
||||
let output = originalOutputs[i];
|
||||
if (outputSelections.some(s => s.cell.persistentId == slicedCell.persistentId && s.outputIndex == i)) {
|
||||
cellJson.outputs.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellJson;
|
||||
}).filter(c => c);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { INotebookTracker, INotebookModel } from "@jupyterlab/notebook";
|
||||
import { GatherModel } from "../packages/gather";
|
||||
import { UUID } from "@phosphor/coreutils";
|
||||
import { log } from "util";
|
||||
|
||||
export function getGatherModelForActiveNotebook(notebooks: INotebookTracker,
|
||||
gatherModelRegistry: GatherModelRegistry): GatherModel | null {
|
||||
let activeNotebook = notebooks.currentWidget;
|
||||
if (activeNotebook == null) return null;
|
||||
return gatherModelRegistry.getGatherModel(activeNotebook.model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all gather models created for all open notebooks.
|
||||
*/
|
||||
export class GatherModelRegistry {
|
||||
|
||||
/**
|
||||
* Returns null is notebook ID is in an unexpected format.
|
||||
*/
|
||||
_getNotebookId(notebookModel: INotebookModel): string | null {
|
||||
const METADATA_NOTEBOOK_ID_KEY = "uuid";
|
||||
if (!notebookModel.metadata.has(METADATA_NOTEBOOK_ID_KEY)) {
|
||||
notebookModel.metadata.set(METADATA_NOTEBOOK_ID_KEY, UUID.uuid4());
|
||||
}
|
||||
let id = notebookModel.metadata.get(METADATA_NOTEBOOK_ID_KEY);
|
||||
if (!(typeof id == 'string')) {
|
||||
log("Unexpected notebook ID format " + id);
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns false if storage of gather model failed.
|
||||
*/
|
||||
addGatherModel(notebookModel : INotebookModel, gatherModel : GatherModel): boolean {
|
||||
let notebookId = this._getNotebookId(notebookModel);
|
||||
if (notebookId == null) return false;
|
||||
this._gatherModels[notebookId] = gatherModel;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if no gather model found for this notebook.
|
||||
*/
|
||||
getGatherModel(notebookModel: INotebookModel) : GatherModel | null {
|
||||
let notebookId = this._getNotebookId(notebookModel);
|
||||
if (notebookId == null) return null;
|
||||
if (this._gatherModels.hasOwnProperty(notebookId)) {
|
||||
return this._gatherModels[notebookId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _gatherModels: { [ notebookId: string ] : GatherModel } = {};
|
||||
}
|
610
src/lab/index.ts
610
src/lab/index.ts
|
@ -1,570 +1,111 @@
|
|||
import { CommandRegistry } from '@phosphor/commands';
|
||||
import { JSONObject } from '@phosphor/coreutils';
|
||||
import { IDisposable, DisposableDelegate } from '@phosphor/disposable';
|
||||
|
||||
import { JupyterLab, JupyterLabPlugin } from '@jupyterlab/application';
|
||||
import { IClientSession, ICommandPalette } from '@jupyterlab/apputils';
|
||||
import { Clipboard as JupyterClipboard } from '@jupyterlab/apputils';
|
||||
import { ICellModel, CodeCell, Cell, CodeCellModel } from '@jupyterlab/cells';
|
||||
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
|
||||
import { IDocumentManager, DocumentManager } from '@jupyterlab/docmanager';
|
||||
import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry';
|
||||
import { FileEditor } from '@jupyterlab/fileeditor';
|
||||
import { NotebookPanel, INotebookModel, Notebook, INotebookTracker } from '@jupyterlab/notebook';
|
||||
import { IObservableList } from '@jupyterlab/observables';
|
||||
|
||||
import { LabCell, copyICodeCellModel } from './LabCell';
|
||||
import { MarkerManager, ICellEditorResolver, ICellOutputResolver, ICellProgramResolver, ICell } from '../packages/cell';
|
||||
import { GatherModel, OutputSelection, GatherController, GatherState } from '../packages/gather';
|
||||
import { NotificationWidget } from '../packages/notification/widget';
|
||||
import { ICommandPalette } from '@jupyterlab/apputils';
|
||||
import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
|
||||
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
||||
import { INotebookModel, INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
|
||||
import { JSONObject } from '@phosphor/coreutils';
|
||||
import { DisposableDelegate, IDisposable } from '@phosphor/disposable';
|
||||
import { loadHistory as loadHistory } from '../history/load';
|
||||
import { storeHistory } from '../history/store';
|
||||
import { MarkerManager } from '../packages/cell';
|
||||
import { GatherController, GatherModel, GatherState } from '../packages/gather';
|
||||
import { DataflowAnalyzer } from '../slicing/DataflowAnalysis';
|
||||
import { ExecutionLogSlicer, SlicedExecution } from '../slicing/ExecutionSlicer';
|
||||
import { CellProgram } from '../slicing/ProgramBuilder';
|
||||
|
||||
import '../../style/lab-vars.css';
|
||||
import '../../style/index.css';
|
||||
import { ICellClipboard, IClipboardListener } from '../packages/gather/clipboard';
|
||||
import { nbformat } from '@jupyterlab/coreutils';
|
||||
import { ExecutionLogSlicer } from '../slicing/ExecutionSlicer';
|
||||
import { log } from '../utils/log';
|
||||
import { INotebookOpener, IScriptOpener } from '../packages/gather/opener';
|
||||
import { ExecutionLogger } from './execution-logger';
|
||||
import { GatherModelRegistry, getGatherModelForActiveNotebook } from './gather-registry';
|
||||
import { NotifactionExtension as NotificationExtension } from './notification';
|
||||
import { ResultsHighlighter } from './results';
|
||||
|
||||
//import { UUID } from '@phosphor/coreutils';
|
||||
import '../../style/index.css';
|
||||
import '../../style/lab-vars.css';
|
||||
import { Clipboard } from './gather-actions';
|
||||
|
||||
/**
|
||||
* Try to only write Jupyter Lab-specific implementation code in this file.
|
||||
* If there is any program analysis / text processing, widgets that could be shared with Jupyter
|
||||
* notebook, try to put those in another shared file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Highlights gatherable entities.
|
||||
*/
|
||||
class ResultsHighlighter {
|
||||
|
||||
private _markerManager: MarkerManager;
|
||||
private _gatherModel: GatherModel;
|
||||
|
||||
constructor(panel: NotebookPanel, gatherModel: GatherModel, markerManager: MarkerManager) {
|
||||
this._markerManager = markerManager;
|
||||
this._gatherModel = gatherModel;
|
||||
|
||||
panel.content.model.cells.changed.connect(
|
||||
(cells, value) =>
|
||||
this.onCellsChanged(panel.content, panel.session, cells, value),
|
||||
this);
|
||||
|
||||
document.body.addEventListener("mouseup", (event: MouseEvent) => {
|
||||
this._markerManager.handleClick(event);
|
||||
});
|
||||
}
|
||||
|
||||
public onCellsChanged(
|
||||
notebook: Notebook,
|
||||
_: IClientSession,
|
||||
__: IObservableList<ICellModel>,
|
||||
cellListChange: IObservableList.IChangedArgs<ICellModel>
|
||||
): void {
|
||||
if (cellListChange.type === 'add') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
if (cellModel.type !== 'code') { return; }
|
||||
|
||||
// When a cell is added, register for its state changes.
|
||||
cellModel.contentChanged.connect((changedCell, args) => {
|
||||
// TODO(andrewhead): check that this change is due to a user's text edit in the cell.
|
||||
if (changedCell instanceof CodeCellModel) {
|
||||
this._gatherModel.lastEditedCell = new LabCell(changedCell);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (cellListChange.type === 'remove') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
if (cellModel instanceof CodeCellModel) {
|
||||
this._gatherModel.lastDeletedCell = new LabCell(cellModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const extension: JupyterLabPlugin<void> = {
|
||||
activate: activateExtension,
|
||||
id: 'gather:gatherPlugin',
|
||||
requires: [ICommandPalette, INotebookTracker, IDocumentManager],
|
||||
autoStart: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension for tracking sequences of cells executed in a notebook.
|
||||
* TODO(andrewhead): have an execution stamp that includes the kernel that executed a cell... (requires insulation in program builder)
|
||||
* TODO(andrewhead): can we run the analysis on the backend with a web-worker (specifically, def-use?)
|
||||
*/
|
||||
class ExecutionLoggerExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
|
||||
class CodeGatheringExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
private _executionSlicer: ExecutionLogSlicer;
|
||||
private _markerManager: MarkerManager;
|
||||
|
||||
constructor(executionSlicer: ExecutionLogSlicer, model: GatherModel, commands: CommandRegistry, markerManager: MarkerManager) {
|
||||
this._gatherModel = model;
|
||||
this._executionSlicer = executionSlicer;
|
||||
this._markerManager = markerManager;
|
||||
constructor(documentManager: DocumentManager, notebooks: INotebookTracker,
|
||||
gatherModelRegistry: GatherModelRegistry) {
|
||||
this._documentManager = documentManager;
|
||||
this._notebooks = notebooks;
|
||||
this._gatherModelRegistry = gatherModelRegistry;
|
||||
}
|
||||
|
||||
get executionSlicer(): ExecutionLogSlicer {
|
||||
return this._executionSlicer;
|
||||
}
|
||||
createNew(notebook: NotebookPanel, notebookContext: DocumentRegistry.IContext<INotebookModel>): IDisposable {
|
||||
|
||||
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
|
||||
new ResultsHighlighter(panel, this._gatherModel, this._markerManager);
|
||||
/*
|
||||
* For the metadata to be available, first wait for the context to be "ready.""
|
||||
*/
|
||||
notebookContext.ready.then(() => {
|
||||
|
||||
panel.content.model.cells.changed.connect(
|
||||
(cells, value) =>
|
||||
this.onCellsChanged(panel.content, panel.session, cells, value),
|
||||
this);
|
||||
let notebookModel = notebookContext.model;
|
||||
let executionLog = new ExecutionLogSlicer(new DataflowAnalyzer());
|
||||
let gatherModel = new GatherModel(executionLog);
|
||||
|
||||
// Listen for all clicks on definition markers to trigger gathering.
|
||||
// XXX: For some reason (tested in both Chrome and Edge), "click" events get dropped
|
||||
// sometimes when you're clicking on a cell. Mouseup doesn't. Eventually should find
|
||||
// the solution to supporting clicks.
|
||||
panel.content.node.addEventListener("mouseup", (event: MouseEvent) => {
|
||||
this._markerManager.handleClick(event);
|
||||
/*
|
||||
* Initialize reactive UI before loading the execution log from storage. This lets us
|
||||
* update the UI automatically as we populate the log.
|
||||
*/
|
||||
let markerManager = new MarkerManager(gatherModel, notebook);
|
||||
new ResultsHighlighter(gatherModel, notebook, markerManager);
|
||||
new GatherController(gatherModel, this._documentManager, this._notebooks);
|
||||
|
||||
this._gatherModelRegistry.addGatherModel(notebookModel, gatherModel);
|
||||
new ExecutionLogger(notebook, gatherModel);
|
||||
saveHistoryOnNotebookSave(notebook, gatherModel);
|
||||
|
||||
loadHistory(notebookContext.model, gatherModel);
|
||||
});
|
||||
|
||||
return new DisposableDelegate(() => { });
|
||||
|
||||
// TODO: listen for reset
|
||||
}
|
||||
|
||||
public onCellsChanged(
|
||||
notebook: Notebook,
|
||||
_: IClientSession,
|
||||
__: IObservableList<ICellModel>,
|
||||
cellListChange: IObservableList.IChangedArgs<ICellModel>
|
||||
): void {
|
||||
if (cellListChange.type === 'add') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
if (cellModel.type !== 'code') { return; }
|
||||
|
||||
// When a cell is added, register for its state changes.
|
||||
cellModel.stateChanged.connect((changedCell, cellStateChange) => {
|
||||
if (changedCell instanceof CodeCellModel && cellStateChange.name === "executionCount" && cellStateChange.newValue !== undefined && cellStateChange.newValue !== null) {
|
||||
let cellClone = copyICodeCellModel(changedCell);
|
||||
const cell = new LabCell(cellClone);
|
||||
this._executionSlicer.logExecution(cell);
|
||||
this._gatherModel.lastExecutedCell = cell;
|
||||
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotifactionExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
|
||||
private notificationWidget: NotificationWidget;
|
||||
|
||||
createNew(panel: NotebookPanel, _: DocumentRegistry.IContext<INotebookModel>): IDisposable {
|
||||
this.notificationWidget = new NotificationWidget();
|
||||
panel.toolbar.insertItem(9, 'notifications', this.notificationWidget);
|
||||
return new DisposableDelegate(() => {
|
||||
this.notificationWidget.dispose();
|
||||
})
|
||||
};
|
||||
|
||||
showMessage(message: string) {
|
||||
this.notificationWidget.showMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
class CellFetcher {
|
||||
|
||||
private _notebooks: INotebookTracker;
|
||||
|
||||
/**
|
||||
* Construct a new cell fetcher.
|
||||
*/
|
||||
constructor(notebooks: INotebookTracker) {
|
||||
this._notebooks = notebooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cell from the notebook with the ID.
|
||||
*/
|
||||
getCellWidgetWithId(cellId: string): Cell {
|
||||
let matchingCell: Cell = null;
|
||||
this._notebooks.forEach((notebook: NotebookPanel) => {
|
||||
if (matchingCell == null) {
|
||||
for (let cell of notebook.content.widgets) {
|
||||
if (cell.model.id == cellId) {
|
||||
matchingCell = cell;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return matchingCell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cell from the notebook with the specified properties.
|
||||
*/
|
||||
getCellWidget(cellId: string, executionCount?: number): Cell {
|
||||
let cell = this.getCellWidgetWithId(cellId);
|
||||
if (cell != null && (cell as CodeCell).model.executionCount == executionCount) {
|
||||
return cell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active editors for cells in Jupyter Lab.
|
||||
*/
|
||||
class LabCellEditorResolver implements ICellEditorResolver {
|
||||
/**
|
||||
* Construct a new cell editor resolver.
|
||||
*/
|
||||
constructor(cellFetcher: CellFetcher) {
|
||||
this._cellFetcher = cellFetcher;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): CodeMirror.Editor {
|
||||
let cellWidget = this._cellFetcher.getCellWidgetWithId(cell.id);
|
||||
return this._getEditor(cellWidget);
|
||||
}
|
||||
|
||||
resolveWithExecutionCount(cell: ICell): CodeMirror.Editor {
|
||||
let cellWidget = this._cellFetcher.getCellWidget(cell.id, cell.executionCount);
|
||||
return this._getEditor(cellWidget);
|
||||
}
|
||||
|
||||
_getEditor(cellWidget: Cell) {
|
||||
if (cellWidget && cellWidget.editor instanceof CodeMirrorEditor) {
|
||||
return cellWidget.editor.editor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _cellFetcher: CellFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds HTML elements for cell outputs in a notebook.
|
||||
*/
|
||||
class LabCellOutputResolver implements ICellOutputResolver {
|
||||
/**
|
||||
* Construct a new cell editor resolver.
|
||||
*/
|
||||
constructor(cellFetcher: CellFetcher) {
|
||||
this._cellFetcher = cellFetcher;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): HTMLElement[] {
|
||||
let cellWidget = this._cellFetcher.getCellWidgetWithId(cell.id);
|
||||
let outputElements = [];
|
||||
if (cellWidget) {
|
||||
let cellElement = cellWidget.node;
|
||||
var outputNodes = cellElement.querySelectorAll(".jp-OutputArea-output");
|
||||
for (var i = 0; i < outputNodes.length; i++) {
|
||||
if (outputNodes[i] instanceof HTMLElement) {
|
||||
outputElements.push(outputNodes[i] as HTMLElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputElements;
|
||||
}
|
||||
|
||||
private _cellFetcher: CellFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps cells to the code analysis information.
|
||||
*/
|
||||
class CellProgramResolver implements ICellProgramResolver {
|
||||
/**
|
||||
* Construct a new cell program resolver
|
||||
*/
|
||||
constructor(executionLogSlicer: ExecutionLogSlicer) {
|
||||
this._executionLogSlicer = executionLogSlicer;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): CellProgram {
|
||||
return this._executionLogSlicer.getCellProgram(cell);
|
||||
}
|
||||
|
||||
private _executionLogSlicer: ExecutionLogSlicer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert program slice to list of cell JSONs
|
||||
*/
|
||||
function sliceToCellJson(slice: SlicedExecution, outputSelections?: OutputSelection[]): nbformat.ICodeCell[] {
|
||||
|
||||
const SHOULD_SLICE_CELLS = true;
|
||||
const OMIT_UNSELECTED_OUTPUT = true;
|
||||
|
||||
outputSelections = outputSelections || [];
|
||||
|
||||
return slice.cellSlices
|
||||
.map((cellSlice, i) => {
|
||||
let slicedCell = cellSlice.cell;
|
||||
if (SHOULD_SLICE_CELLS) {
|
||||
slicedCell = slicedCell.copy();
|
||||
slicedCell.text = cellSlice.textSliceLines;
|
||||
}
|
||||
if (slicedCell instanceof LabCell) {
|
||||
let cellJson = slicedCell.toJSON();
|
||||
// This new cell hasn't been executed yet. So don't mark it as having been executed.
|
||||
cellJson.execution_count = null;
|
||||
// Add a flag to distinguish gathered cells from other cells.
|
||||
cellJson.metadata.gathered = true;
|
||||
// Filter to just those outputs that were selected.
|
||||
if (OMIT_UNSELECTED_OUTPUT) {
|
||||
let originalOutputs = cellJson.outputs;
|
||||
cellJson.outputs = [];
|
||||
for (let i = 0; i < originalOutputs.length; i++) {
|
||||
let output = originalOutputs[i];
|
||||
if (outputSelections.some(s => s.cell.id == slicedCell.id && s.outputIndex == i)) {
|
||||
cellJson.outputs.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellJson;
|
||||
}
|
||||
}).filter(c => c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new notebooks containing program slices.
|
||||
*/
|
||||
class NotebookOpener implements INotebookOpener {
|
||||
|
||||
constructor(documentManager: DocumentManager, notebooks: INotebookTracker) {
|
||||
this._documentManager = documentManager;
|
||||
this._notebooks = notebooks;
|
||||
}
|
||||
|
||||
openNotebookForSlice(slice: SlicedExecution) {
|
||||
|
||||
// TODO give this new document a better name than "Untitled".
|
||||
this._documentManager.newUntitled({ ext: 'ipynb' }).then(model => {
|
||||
// TODO put more safety checks on this
|
||||
const widget = this._documentManager.open(model.path, undefined, this._notebooks.currentWidget.session.kernel.model) as NotebookPanel;
|
||||
setTimeout(() => {
|
||||
|
||||
const notebookModel = widget.content.model;
|
||||
let notebookJson = notebookModel.toJSON() as nbformat.INotebookContent;
|
||||
notebookJson.cells = []
|
||||
if (slice) {
|
||||
let cellsJson = sliceToCellJson(slice, []);
|
||||
for (let cell of cellsJson) {
|
||||
notebookJson.cells.push(cell);
|
||||
}
|
||||
}
|
||||
notebookModel.fromJSON(notebookJson);
|
||||
// XXX can we make this work without the 100-ms delay?
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
private _documentManager: DocumentManager;
|
||||
private _notebooks: INotebookTracker;
|
||||
private _gatherModelRegistry: GatherModelRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new scripts containing program slices.
|
||||
*/
|
||||
class ScriptOpener implements IScriptOpener {
|
||||
|
||||
constructor(documentManager: DocumentManager, notebooks: INotebookTracker) {
|
||||
this._documentManager = documentManager;
|
||||
this._notebooks = notebooks;
|
||||
}
|
||||
|
||||
openScriptForSlice(slice: SlicedExecution) {
|
||||
|
||||
// TODO give this new document a better name than "Untitled".
|
||||
this._documentManager.newUntitled({ ext: 'py' }).then(model => {
|
||||
// TODO put more safety checks on this
|
||||
const editor = this._documentManager.open(model.path, undefined, this._notebooks.currentWidget.session.kernel.model) as IDocumentWidget<FileEditor>;
|
||||
setTimeout(() => {
|
||||
if (slice) {
|
||||
let cellsJson = sliceToCellJson(slice, []);
|
||||
let scriptText = cellsJson.map(cellJson => cellJson.source).join("\n");
|
||||
editor.content.model.value.text = scriptText;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
private _documentManager: DocumentManager;
|
||||
private _notebooks: INotebookTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather code to the clipboard.
|
||||
* Logic copied from packages/notebooks/src/actions.tsx in Jupyter Lab project.
|
||||
*/
|
||||
class Clipboard implements ICellClipboard {
|
||||
|
||||
constructor(gatherModel: GatherModel) {
|
||||
this._gatherModel = gatherModel;
|
||||
}
|
||||
|
||||
addListener(listener: IClipboardListener) {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
|
||||
copy(slice: SlicedExecution) {
|
||||
const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
|
||||
if (slice) {
|
||||
let cellJson = sliceToCellJson(slice, this._gatherModel.selectedOutputs.concat());
|
||||
const clipboard = JupyterClipboard.getInstance();
|
||||
clipboard.clear();
|
||||
clipboard.setData(JUPYTER_CELL_MIME, cellJson);
|
||||
function saveHistoryOnNotebookSave(notebook: NotebookPanel, gatherModel: GatherModel) {
|
||||
notebook.context.saveState.connect((_, message) => {
|
||||
if (message == 'started') {
|
||||
storeHistory(notebook.model, gatherModel.executionLog);
|
||||
}
|
||||
this._listeners.forEach(listener => listener.onCopy(slice, this));
|
||||
}
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
private _listeners: IClipboardListener[] = [];
|
||||
}
|
||||
|
||||
function activateExtension(app: JupyterLab, palette: ICommandPalette, notebooks: INotebookTracker, docManager: IDocumentManager) {
|
||||
|
||||
console.log('Activating code gathering tools');
|
||||
|
||||
|
||||
docManager.activateRequested.connect(
|
||||
(docMan, msg) => {
|
||||
notebooks.forEach((widget: NotebookPanel) => print(widget))
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function activateExtension(app: JupyterLab, palette: ICommandPalette, notebooks: INotebookTracker, documentManager: IDocumentManager) {
|
||||
|
||||
let gatherModel = new GatherModel();
|
||||
console.log('Activating code gathering tools...');
|
||||
|
||||
const notificationExtension = new NotifactionExtension();
|
||||
const notificationExtension = new NotificationExtension();
|
||||
app.docRegistry.addWidgetExtension('Notebook', notificationExtension);
|
||||
|
||||
let executionSlicer = new ExecutionLogSlicer(new DataflowAnalyzer());
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let historyLoaded = 0;
|
||||
let previousSaveLogLength = executionSlicer._executionLog.length
|
||||
function print(notebook: NotebookPanel){
|
||||
|
||||
//deserializes execution history on load of the notebook metadata
|
||||
if (executionSlicer._executionLog.length == 0 && historyLoaded == 0) {
|
||||
var seconds = 0;
|
||||
var intervalId = setInterval(function(){
|
||||
seconds+=1
|
||||
if(seconds == 10) {
|
||||
console.log("history is empty")
|
||||
clearInterval(intervalId);
|
||||
historyLoaded = 1;
|
||||
} else {
|
||||
|
||||
if (notebook.model.metadata.get("executionHistory")) {
|
||||
let history = notebook.model.metadata.get("executionHistory");
|
||||
|
||||
console.log("History Loading...")
|
||||
console.log("Loading Before", executionSlicer._executionLog);
|
||||
console.log("Notebook Stored Execution History", notebook.model.metadata.get("executionHistory"));
|
||||
|
||||
for (let x in (<any>history)){
|
||||
let cell =(<ICell>(<any>history)[x.toString()]);
|
||||
|
||||
executionSlicer.logExecution(cell)
|
||||
}
|
||||
//notebook.model.metadata.set("executionHistory","")
|
||||
historyLoaded = 1;
|
||||
previousSaveLogLength = executionSlicer._executionLog.length
|
||||
console.log("Loading After", executionSlicer._executionLog);
|
||||
clearInterval(intervalId);
|
||||
} else {
|
||||
console.log(notebook.model.metadata.get("executionHistory"));
|
||||
console.log("still waiting!", seconds);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
//Notebook listener serializes execution log to metadata on save
|
||||
notebook.context.saveState.connect((context, msg) => {
|
||||
|
||||
let currentSaveLogLength = executionSlicer._programBuilder._cellPrograms.length
|
||||
if (msg == "started" && currentSaveLogLength != previousSaveLogLength){
|
||||
|
||||
console.log("Saving File and Updating Execution History...");
|
||||
console.log("before", notebook.model.metadata.get("executionHistory"));
|
||||
|
||||
let cellPrograms = executionSlicer._programBuilder._cellPrograms;
|
||||
let tempCellGroup: any = [];
|
||||
|
||||
for (var i = previousSaveLogLength; i < cellPrograms.length; i++) {
|
||||
let cell = cellPrograms[i].cell;
|
||||
let tempCell: any = {};
|
||||
tempCell["id"] = cell.id;
|
||||
tempCell["is_cell"] = cell.is_cell;
|
||||
tempCell["executionCount"] = cell.executionCount;
|
||||
tempCell["hasError"] = cell.hasError;
|
||||
tempCell["isCode"] = cell.isCode;
|
||||
tempCell["text"] = cell.text;
|
||||
tempCell["gathered"] = cell.gathered;
|
||||
tempCellGroup.push(tempCell);
|
||||
}
|
||||
|
||||
let history = notebook.model.metadata.get("executionHistory");
|
||||
let tempHistory: any = {};
|
||||
let counter = 0
|
||||
|
||||
for (let x in (<any>history)){
|
||||
tempHistory[counter.toString()] = (<any>history)[x.toString()];
|
||||
counter += 1;
|
||||
}
|
||||
for (var i = 0; i<tempCellGroup.length; i++) {
|
||||
tempHistory[counter.toString()] = tempCellGroup[i];
|
||||
counter+=1;
|
||||
}
|
||||
|
||||
previousSaveLogLength = currentSaveLogLength;
|
||||
notebook.model.metadata.set("executionHistory",tempHistory)
|
||||
|
||||
console.log("after", notebook.model.metadata.get("executionHistory"));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
let notebookOpener = new NotebookOpener(docManager, notebooks);
|
||||
let scriptOpener = new ScriptOpener(docManager, notebooks);
|
||||
|
||||
// Initialize clipboard for copying cells.
|
||||
let clipboard = new Clipboard(gatherModel);
|
||||
clipboard.addListener({
|
||||
onCopy: () => {
|
||||
notificationExtension.showMessage("Copied cells to clipboard.");
|
||||
}
|
||||
Clipboard.getInstance().copied.connect(() => {
|
||||
notificationExtension.showMessage("Copied cells to clipboard.");
|
||||
});
|
||||
|
||||
// Controller for global UI state.
|
||||
new GatherController(gatherModel, executionSlicer, clipboard, notebookOpener, scriptOpener);
|
||||
|
||||
let cellFetcher = new CellFetcher(notebooks);
|
||||
let cellProgramResolver = new CellProgramResolver(executionSlicer);
|
||||
let cellEditorResolver = new LabCellEditorResolver(cellFetcher);
|
||||
let cellOutputResolver = new LabCellOutputResolver(cellFetcher);
|
||||
let markerManager = new MarkerManager(gatherModel, cellProgramResolver, cellEditorResolver, cellOutputResolver);
|
||||
|
||||
const executionLogger = new ExecutionLoggerExtension(executionSlicer, gatherModel, app.commands, markerManager);
|
||||
app.docRegistry.addWidgetExtension('Notebook', executionLogger);
|
||||
|
||||
|
||||
let gatherModelRegistry = new GatherModelRegistry();
|
||||
app.docRegistry.addWidgetExtension('Notebook', new CodeGatheringExtension(documentManager, notebooks, gatherModelRegistry));
|
||||
|
||||
function addCommand(command: string, label: string, execute: (options?: JSONObject) => void) {
|
||||
app.commands.addCommand(command, { label, execute });
|
||||
palette.addItem({ command, category: 'Clean Up' });
|
||||
}
|
||||
|
||||
addCommand('gather:gatherToClipboard', 'Gather this result to the clipboard', (options: JSONObject) => {
|
||||
addCommand('gather:gatherToClipboard', 'Gather this result to the clipboard', () => {
|
||||
let gatherModel = getGatherModelForActiveNotebook(notebooks, gatherModelRegistry);
|
||||
if (gatherModel == null) return;
|
||||
log("Button: Clicked gather to notebook with selections", {
|
||||
selectedDefs: gatherModel.selectedDefs,
|
||||
selectedOutputs: gatherModel.selectedOutputs });
|
||||
|
@ -573,6 +114,8 @@ function activateExtension(app: JupyterLab, palette: ICommandPalette, notebooks:
|
|||
});
|
||||
|
||||
addCommand('gather:gatherToNotebook', 'Gather this result into a new notebook', () => {
|
||||
let gatherModel = getGatherModelForActiveNotebook(notebooks, gatherModelRegistry);
|
||||
if (gatherModel == null) return;
|
||||
if (gatherModel.selectedSlices.length >= 1) {
|
||||
log("Button: Clicked gather to notebook with selections", {
|
||||
selectedDefs: gatherModel.selectedDefs,
|
||||
|
@ -583,6 +126,8 @@ function activateExtension(app: JupyterLab, palette: ICommandPalette, notebooks:
|
|||
});
|
||||
|
||||
addCommand('gather:gatherToScript', 'Gather this result into a new script', () => {
|
||||
let gatherModel = getGatherModelForActiveNotebook(notebooks, gatherModelRegistry);
|
||||
if (gatherModel == null) return;
|
||||
if (gatherModel.selectedSlices.length >= 1) {
|
||||
log("Button: Clicked gather to script with selections", {
|
||||
selectedDefs: gatherModel.selectedDefs,
|
||||
|
@ -618,14 +163,7 @@ function activateExtension(app: JupyterLab, palette: ICommandPalette, notebooks:
|
|||
});
|
||||
*/
|
||||
|
||||
console.log('Activated code gathering tools.');
|
||||
console.log('Code gathering tools have been activated.');
|
||||
}
|
||||
|
||||
const extension: JupyterLabPlugin<void> = {
|
||||
activate: activateExtension,
|
||||
id: 'gather:gatherPlugin',
|
||||
requires: [ICommandPalette, INotebookTracker, IDocumentManager],
|
||||
autoStart: true
|
||||
};
|
||||
|
||||
export default extension;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AbstractOutputterCell } from "../packages/cell";
|
||||
import { ICodeCellModel, CodeCellModel } from "@jupyterlab/cells";
|
||||
import { IOutputModel } from "@jupyterlab/rendermime";
|
||||
import { UUID } from "@phosphor/coreutils";
|
||||
|
||||
/**
|
||||
* Create a new cell with the same ID and content.
|
||||
|
@ -27,6 +28,13 @@ export class LabCell extends AbstractOutputterCell<IOutputModel[]> {
|
|||
return this._model.id;
|
||||
}
|
||||
|
||||
get persistentId(): string {
|
||||
if (!this._model.metadata.has("persistent_id")) {
|
||||
this._model.metadata.set("persistent_id", UUID.uuid4());
|
||||
}
|
||||
return this._model.metadata.get("persistent_id") as string;
|
||||
}
|
||||
|
||||
get text(): string {
|
||||
return this._model.value.text;
|
||||
}
|
||||
|
@ -70,7 +78,7 @@ export class LabCell extends AbstractOutputterCell<IOutputModel[]> {
|
|||
return new LabCell(clonedModel);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
toJupyterJSON(): any {
|
||||
return this._model.toJSON();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { DocumentRegistry } from "@jupyterlab/docregistry";
|
||||
import { NotebookPanel, INotebookModel } from "@jupyterlab/notebook";
|
||||
import { IDisposable, DisposableDelegate } from "@phosphor/disposable";
|
||||
import { NotificationWidget } from "../packages/notification";
|
||||
|
||||
export class NotifactionExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
|
||||
private notificationWidget: NotificationWidget;
|
||||
|
||||
createNew(panel: NotebookPanel, _: DocumentRegistry.IContext<INotebookModel>): IDisposable {
|
||||
this.notificationWidget = new NotificationWidget();
|
||||
panel.toolbar.insertItem(9, 'notifications', this.notificationWidget);
|
||||
return new DisposableDelegate(() => {
|
||||
this.notificationWidget.dispose();
|
||||
})
|
||||
};
|
||||
|
||||
showMessage(message: string) {
|
||||
this.notificationWidget.showMessage(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { MarkerManager } from "../packages/cell";
|
||||
import { GatherModel } from "../packages/gather";
|
||||
import { NotebookPanel, Notebook } from "@jupyterlab/notebook";
|
||||
import { IClientSession } from "@jupyterlab/apputils";
|
||||
import { IObservableList } from "@jupyterlab/observables";
|
||||
import { ICellModel, CodeCellModel } from "@jupyterlab/cells";
|
||||
import { LabCell } from "./LabCell";
|
||||
|
||||
/**
|
||||
* Highlights gatherable entities.
|
||||
*/
|
||||
export class ResultsHighlighter {
|
||||
|
||||
private _markerManager: MarkerManager;
|
||||
private _gatherModel: GatherModel;
|
||||
|
||||
constructor(gatherModel: GatherModel, panel: NotebookPanel, markerManager: MarkerManager) {
|
||||
this._markerManager = markerManager;
|
||||
this._gatherModel = gatherModel;
|
||||
|
||||
panel.content.model.cells.changed.connect(
|
||||
(cells, value) =>
|
||||
this.onCellsChanged(panel.content, panel.session, cells, value),
|
||||
this);
|
||||
|
||||
document.body.addEventListener("mouseup", (event: MouseEvent) => {
|
||||
this._markerManager.handleClick(event);
|
||||
});
|
||||
}
|
||||
|
||||
public onCellsChanged(
|
||||
notebook: Notebook,
|
||||
_: IClientSession,
|
||||
__: IObservableList<ICellModel>,
|
||||
cellListChange: IObservableList.IChangedArgs<ICellModel>
|
||||
): void {
|
||||
if (cellListChange.type === 'add') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
if (cellModel.type !== 'code') { return; }
|
||||
|
||||
// When a cell is added, register for its state changes.
|
||||
cellModel.contentChanged.connect((changedCell, args) => {
|
||||
// TODO(andrewhead): check that this change is due to a user's text edit in the cell.
|
||||
if (changedCell instanceof CodeCellModel) {
|
||||
this._gatherModel.lastEditedCell = new LabCell(changedCell);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (cellListChange.type === 'remove') {
|
||||
const cellModel = cellListChange.newValues[0] as ICellModel;
|
||||
if (cellModel instanceof CodeCellModel) {
|
||||
this._gatherModel.lastDeletedCell = new LabCell(cellModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
*.bundle.*
|
||||
lib/
|
||||
node_modules/
|
||||
*.egg-info/
|
||||
.ipynb_checkpoints
|
||||
.vscode/
|
||||
.vs/
|
||||
package-lock.json
|
|
@ -1,137 +0,0 @@
|
|||
import { AbstractOutputterCell } from "../packages/cell";
|
||||
import { CodeCell, OutputArea, notebook, Cell } from 'base/js/namespace';
|
||||
|
||||
/**
|
||||
* Create a new cell with the same ID and content.
|
||||
*/
|
||||
export function copyCodeCell(cell: CodeCell): CodeCell {
|
||||
let cellClone = new CodeCell(cell.kernel, {
|
||||
config: notebook.config,
|
||||
notebook: cell.notebook,
|
||||
events: cell.events,
|
||||
keyboard_manager: cell.keyboard_manager,
|
||||
tooltip: cell.tooltip
|
||||
});
|
||||
cellClone.fromJSON(cell.toJSON());
|
||||
cellClone.cell_id = cell.cell_id;
|
||||
return cellClone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of SliceableCell for Jupyter Lab. Wrapper around the ICodeCellModel.
|
||||
*/
|
||||
export class NotebookCell extends AbstractOutputterCell<OutputArea> {
|
||||
|
||||
constructor(model: CodeCell) {
|
||||
super();
|
||||
this._model = model;
|
||||
}
|
||||
|
||||
get model(): CodeCell {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._model.cell_id;
|
||||
}
|
||||
|
||||
get text(): string {
|
||||
return this._model.code_mirror.getValue();
|
||||
}
|
||||
|
||||
set text(text: string) {
|
||||
this._model.code_mirror.setValue(text);
|
||||
}
|
||||
|
||||
get executionCount(): number {
|
||||
return this._model.input_prompt_number;
|
||||
}
|
||||
|
||||
set executionCount(count: number) {
|
||||
this._model.input_prompt_number = count;
|
||||
}
|
||||
|
||||
get isCode(): boolean {
|
||||
return this._model.cell_type == "code";
|
||||
}
|
||||
|
||||
get hasError(): boolean {
|
||||
return this.output.outputs.some(o => o.output_type === 'error');
|
||||
}
|
||||
|
||||
get editor(): CodeMirror.Editor {
|
||||
return this._model.code_mirror;
|
||||
}
|
||||
|
||||
get output(): OutputArea {
|
||||
if (this._model.output_area) {
|
||||
return this._model.output_area;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get gathered(): boolean {
|
||||
if (this._model.metadata && this._model.metadata.gathered) {
|
||||
return this._model.metadata.gathered;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
copy(): NotebookCell {
|
||||
return new NotebookCell(copyCodeCell(this._model));
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
let baseJson = super.toJSON();
|
||||
baseJson.output = getCellOutputLogData(this.output);
|
||||
}
|
||||
|
||||
is_cell: boolean = true;
|
||||
is_outputter_cell: boolean = true;
|
||||
private _model: CodeCell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON for a Jupyter notebook internal representation of an output area.
|
||||
*/
|
||||
function getCellOutputLogData(outputArea: OutputArea) {
|
||||
// TODO: consider checking for HTML tables.
|
||||
let outputData = [];
|
||||
if (outputArea && outputArea.outputs && outputArea.outputs.length > 0) {
|
||||
for (let output of outputArea.outputs) {
|
||||
let type = output.output_type;
|
||||
let mimeTags: string[] = [];
|
||||
let data = output.data;
|
||||
if (data && Object.keys(data)) {
|
||||
mimeTags = Object.keys(data);
|
||||
}
|
||||
outputData.push({ type, mimeTags });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from Jupyter notebook's internal cell representation to an unsensitized summary
|
||||
* of the cell's contents.
|
||||
*/
|
||||
export function nbCellToJson(cell: Cell): any {
|
||||
if (cell instanceof CodeCell) {
|
||||
return {
|
||||
type: "code",
|
||||
id: cell.cell_id,
|
||||
executionCount: cell.input_prompt_number,
|
||||
lineCount: cell.code_mirror.getValue().split("\n").length,
|
||||
gathered: cell.metadata && cell.metadata.gathered,
|
||||
output: getCellOutputLogData(cell.output_area)
|
||||
}
|
||||
} else if (cell instanceof Cell) {
|
||||
return {
|
||||
type: "other",
|
||||
id: cell.cell_id,
|
||||
executionCount: null,
|
||||
lineCount: cell.code_mirror.getValue().split("\n").length,
|
||||
gathered: cell.metadata && cell.metadata.gathered
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
import { Widget, PanelLayout } from "@phosphor/widgets";
|
||||
import { IGatherObserver, GatherModel, GatherModelEvent, GatherEventData, GatherState } from "../packages/gather";
|
||||
import { buildHistoryModel, HistoryViewer } from "../packages/history";
|
||||
import { IOutputRenderer } from "../packages/revision";
|
||||
import { OutputArea } from "base/js/namespace";
|
||||
import { log } from "../utils/log";
|
||||
|
||||
|
||||
/**
|
||||
* Class for the revision browser widget.
|
||||
*/
|
||||
const REVISION_BROWSER_CLASS = "jp-Notebook-revisionbrowser";
|
||||
|
||||
/**
|
||||
* Class for output areas in the revision browser.
|
||||
*/
|
||||
const REVISION_OUTPUT_CLASS = "jp-Notebook-revisionbrowser-output";
|
||||
|
||||
|
||||
/**
|
||||
* Renders output models for notebooks as new cells.
|
||||
*/
|
||||
class OutputRenderer implements IOutputRenderer<OutputArea> {
|
||||
/**
|
||||
* Render HTML element for this output.
|
||||
*/
|
||||
render(output: OutputArea): HTMLElement {
|
||||
let clone = $(output.element[0].cloneNode(true));
|
||||
// Remove output prompts to make it more pretty.
|
||||
clone.find("div.prompt").remove();
|
||||
clone.find("div.run_this_cell").remove();
|
||||
clone.addClass(REVISION_OUTPUT_CLASS);
|
||||
return clone[0] as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Window that lets the user browse revisions of code.
|
||||
*/
|
||||
export class RevisionBrowser extends Widget implements IGatherObserver {
|
||||
/**
|
||||
* Construct a new revision browser.
|
||||
*/
|
||||
constructor(gatherModel: GatherModel) {
|
||||
super();
|
||||
this.addClass(REVISION_BROWSER_CLASS);
|
||||
|
||||
gatherModel.addObserver(this);
|
||||
this._gatherModel = gatherModel;
|
||||
this._outputRenderer = new OutputRenderer();
|
||||
|
||||
// Add button for exiting the revision browser.
|
||||
let exitButton = document.createElement("div");
|
||||
let icon = document.createElement("i");
|
||||
icon.classList.add("fa", "fa-close");
|
||||
exitButton.appendChild(icon);
|
||||
// exitButton.textContent = "X";
|
||||
exitButton.onclick = () => { this.dismiss(); };
|
||||
let exitWidget = new Widget({ node: exitButton });
|
||||
exitWidget.addClass("jp-Notebook-revisionbrowser-exit");
|
||||
let layout = (this.layout = new PanelLayout());
|
||||
layout.addWidget(exitWidget);
|
||||
|
||||
// This widget starts out hidden.
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle change to the gather model.
|
||||
*/
|
||||
onModelChange(eventType: GatherModelEvent, eventData: GatherEventData, model: GatherModel) {
|
||||
if (eventType == GatherModelEvent.STATE_CHANGED) {
|
||||
let newState = eventData as GatherState;
|
||||
if (newState == GatherState.GATHER_HISTORY) {
|
||||
this.show();
|
||||
this.attachSliceWidgets(model);
|
||||
} else {
|
||||
this.hide();
|
||||
if (this._historyViewer) {
|
||||
this.layout.removeWidget(this._historyViewer);
|
||||
this._historyViewer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachSliceWidgets(model: GatherModel) {
|
||||
let defSelections = model.selectedDefs;
|
||||
let outputSelections = model.selectedOutputs;
|
||||
let slices;
|
||||
let cellId;
|
||||
if (defSelections.length > 0) {
|
||||
slices = model.getSelectedDefSlices(defSelections[0]);
|
||||
cellId = defSelections[0].cell.id;
|
||||
} else if (outputSelections.length > 0) {
|
||||
slices = model.getSelectedOutputSlices(outputSelections[0]);
|
||||
cellId = outputSelections[0].cell.id;
|
||||
}
|
||||
log("Bringing up the revision browser for selection", {
|
||||
cellId, slices,
|
||||
selectedDefs: model.selectedDefs,
|
||||
selectedOutputs: model.selectedOutputs
|
||||
});
|
||||
if (slices && cellId) {
|
||||
// Only show output if the selection was output.
|
||||
let includeOutput = model.selectedOutputs.length >= 1;
|
||||
let historyModel = buildHistoryModel<OutputArea>(
|
||||
model, cellId, slices, includeOutput);
|
||||
// This currently uses code borrowed from Jupyter Lab (for rendering MIME and creating
|
||||
// the default editor factory). Not ideal. Fix up soon.
|
||||
let historyViewer = new HistoryViewer<OutputArea>({
|
||||
model: historyModel,
|
||||
outputRenderer: this._outputRenderer
|
||||
});
|
||||
this._historyViewer = historyViewer;
|
||||
(this.layout as PanelLayout).addWidget(historyViewer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss this widget.
|
||||
*/
|
||||
dismiss() {
|
||||
log("Dismissing revision browser");
|
||||
this._gatherModel.requestStateChange(GatherState.SELECTING);
|
||||
}
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
private _outputRenderer: OutputRenderer;
|
||||
private _historyViewer: HistoryViewer<OutputArea>;
|
||||
}
|
|
@ -1,290 +0,0 @@
|
|||
import { GatherModel, IGatherObserver, GatherModelEvent, GatherEventData, GatherState } from "../packages/gather";
|
||||
import { Widget } from "@phosphor/widgets";
|
||||
import { Action, Actions, Notebook } from "base/js/namespace";
|
||||
import { log } from "../utils/log";
|
||||
import { nbCellToJson } from "./NotebookCell";
|
||||
|
||||
|
||||
/**
|
||||
* Button to add to the Jupyter notebook toolbar.
|
||||
*/
|
||||
interface Button {
|
||||
label?: string;
|
||||
actionName: string;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for highlighted buttons.
|
||||
*/
|
||||
const HIGHLIGHTED_BUTTON_CLASS = "jp-Toolbar-button-glow";
|
||||
|
||||
/**
|
||||
* Button for merging selected cells.
|
||||
*/
|
||||
export class MergeButton implements Button {
|
||||
/**
|
||||
* Properties of the merge action.
|
||||
*/
|
||||
readonly CLASS_NAME = "jp-Toolbar-mergebutton";
|
||||
readonly label = "Merge";
|
||||
readonly actionName = "merge-cells";
|
||||
readonly action = {
|
||||
icon: 'fa-compress',
|
||||
help: 'Merge selected cells',
|
||||
help_index: 'merge-cells',
|
||||
handler: () => {
|
||||
let selectedCells = this._notebook.get_selected_cells();
|
||||
log("Button: Merging cells", {
|
||||
selectedCells: selectedCells.map(c => nbCellToJson(c))
|
||||
});
|
||||
this._actions.call("jupyter-notebook:merge-cells");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a merge button.
|
||||
*/
|
||||
constructor(actions: Actions, notebook: Notebook) {
|
||||
this._actions = actions;
|
||||
this._notebook = notebook;
|
||||
setInterval(this.updateState.bind(this), 100);
|
||||
}
|
||||
|
||||
updateState() {
|
||||
// Only enable this button if there is more than one selected...
|
||||
let selectedCells = this._notebook.get_selected_cells();
|
||||
this.disabled = (selectedCells.length <= 1);
|
||||
}
|
||||
|
||||
set disabled(disabled: boolean) {
|
||||
this._disabled = disabled;
|
||||
if (this._node) {
|
||||
(this._node.node as HTMLButtonElement).disabled = this._disabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the node for this button. For now, has to be done after initialization, given how
|
||||
* Jupyter notebook initializes toolbars.
|
||||
*/
|
||||
set node(node: Widget) {
|
||||
if (this._node != node) {
|
||||
this._node = node;
|
||||
this._node.addClass(this.CLASS_NAME);
|
||||
this.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _actions: Actions;
|
||||
private _disabled: boolean;
|
||||
private _notebook: Notebook;
|
||||
private _node: Widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for buttons that highlight on model change.
|
||||
*/
|
||||
abstract class GatherButton implements Button, IGatherObserver {
|
||||
|
||||
readonly BASE_CLASS_NAME = "jp-Toolbar-gatherbutton";
|
||||
readonly DISABLED_CLASS_NAME = "jp-Toolbar-gatherbutton-inactive";
|
||||
abstract readonly CLASS_NAME: string;
|
||||
abstract readonly label?: string;
|
||||
abstract readonly actionName: string;
|
||||
abstract readonly action: Action;
|
||||
|
||||
/**
|
||||
* Construct a gather button.
|
||||
*/
|
||||
constructor(gatherModel: GatherModel) {
|
||||
this._gatherModel = gatherModel;
|
||||
this._gatherModel.addObserver(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the node for this button. For now, has to be done after initialization, given how
|
||||
* Jupyter notebook initializes toolbars.
|
||||
*/
|
||||
set node(node: Widget) {
|
||||
if (this._widget != node) {
|
||||
this._widget = node;
|
||||
this._widget.addClass(this.BASE_CLASS_NAME);
|
||||
this._widget.addClass(this.CLASS_NAME);
|
||||
this._updateDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateDisabled() {
|
||||
if (this._gatherModel.selectedSlices.length > 0) {
|
||||
if (this._widget) {
|
||||
this._widget.removeClass(this.DISABLED_CLASS_NAME);
|
||||
this._widget.addClass(HIGHLIGHTED_BUTTON_CLASS);
|
||||
}
|
||||
} else {
|
||||
if (this._widget) {
|
||||
this._widget.addClass(this.DISABLED_CLASS_NAME);
|
||||
this._widget.removeClass(HIGHLIGHTED_BUTTON_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for changes on the gather model.
|
||||
*/
|
||||
onModelChange(event: GatherModelEvent, eventData: GatherEventData, model: GatherModel) {
|
||||
if (event == GatherModelEvent.SLICE_SELECTED || event == GatherModelEvent.SLICE_DESELECTED) {
|
||||
this._updateDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
protected _gatherModel: GatherModel;
|
||||
protected _widget: Widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to gather code to the clipboard.
|
||||
*/
|
||||
export class GatherToClipboardButton extends GatherButton {
|
||||
/**
|
||||
* Properties for initializing the gather button.
|
||||
*/
|
||||
readonly CLASS_NAME = "jp-Toolbar-gathertoclipboardbutton";
|
||||
// readonly label = "Cells";
|
||||
readonly label = "Clipboard";
|
||||
readonly actionName = "gather-to-clipboard";
|
||||
readonly action = {
|
||||
icon: 'fa-clipboard',
|
||||
help: 'Gather code to clipboard',
|
||||
help_index: 'gather-to-clipboard',
|
||||
handler: () => { this.onClick() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click action.
|
||||
*/
|
||||
onClick() {
|
||||
if (this._gatherModel.selectedSlices.length >= 1) {
|
||||
log("Button: Clicked gather to clipboard with selections", {
|
||||
selectedDefs: this._gatherModel.selectedDefs,
|
||||
selectedOutputs: this._gatherModel.selectedOutputs });
|
||||
this._gatherModel.addChosenSlices(...this._gatherModel.selectedSlices.map(sel => sel.slice));
|
||||
this._gatherModel.requestStateChange(GatherState.GATHER_TO_CLIPBOARD);
|
||||
} else {
|
||||
log("Button: Clicked gather to clipboard without selections");
|
||||
window.alert("Before you gather, click on one of the blue variable names, or one of the outputs with a blue border.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to gather code to the clipboard.
|
||||
*/
|
||||
export class GatherToNotebookButton extends GatherButton {
|
||||
/**
|
||||
* Properties for initializing the gather button.
|
||||
*/
|
||||
readonly CLASS_NAME = "jp-Toolbar-gathertonotebookbutton";
|
||||
readonly label = "Notebook";
|
||||
readonly actionName = "gather-to-notebook";
|
||||
readonly action = {
|
||||
icon: 'fa-book',
|
||||
help: 'Gather code to new notebook',
|
||||
help_index: 'gather-to-notebook',
|
||||
handler: () => { this.onClick() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click action.
|
||||
*/
|
||||
onClick() {
|
||||
if (this._gatherModel.selectedSlices.length >= 1) {
|
||||
log("Button: Clicked gather to notebook with selections", {
|
||||
selectedDefs: this._gatherModel.selectedDefs,
|
||||
selectedOutputs: this._gatherModel.selectedOutputs });
|
||||
this._gatherModel.addChosenSlices(...this._gatherModel.selectedSlices.map(sel => sel.slice));
|
||||
this._gatherModel.requestStateChange(GatherState.GATHER_TO_NOTEBOOK);
|
||||
} else {
|
||||
log("Button: Clicked gather to clipboard without selections");
|
||||
window.alert("Before you gather, click on one of the blue variable names, or one of the outputs with a blue border.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to gather and display versions of code.
|
||||
*/
|
||||
export class GatherHistoryButton extends GatherButton {
|
||||
/**
|
||||
* Properties for initializing the gather button.
|
||||
*/
|
||||
readonly CLASS_NAME = "jp-Toolbar-gatherhistorybutton";
|
||||
readonly label = "Revisions";
|
||||
readonly actionName = "gather-history";
|
||||
readonly action = {
|
||||
icon: 'fa-history',
|
||||
help: 'Gather versions of this code',
|
||||
help_index: 'gather-history',
|
||||
handler: () => { this.onClick() }
|
||||
}
|
||||
|
||||
protected _updateDisabled() {
|
||||
if (this._gatherModel.selectedSlices.length == 1) {
|
||||
if (this._widget) {
|
||||
this._widget.addClass(HIGHLIGHTED_BUTTON_CLASS);
|
||||
this._widget.removeClass(this.DISABLED_CLASS_NAME);
|
||||
}
|
||||
} else {
|
||||
if (this._widget) {
|
||||
this._widget.removeClass(HIGHLIGHTED_BUTTON_CLASS);
|
||||
this._widget.addClass(this.DISABLED_CLASS_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click action.
|
||||
*/
|
||||
onClick() {
|
||||
if (this._gatherModel.selectedSlices.length == 1) {
|
||||
log("Button: Clicked gather to history with a selection", {
|
||||
selectedDefs: this._gatherModel.selectedDefs,
|
||||
selectedOutputs: this._gatherModel.selectedOutputs });
|
||||
this._gatherModel.requestStateChange(GatherState.GATHER_HISTORY);
|
||||
} else if (this._gatherModel.selectedSlices.length == 0) {
|
||||
log("Button: Clicked gather to history without any selections");
|
||||
window.alert("Before bringing up a history, first click on one of the blue variables, or one of the outputs with a blue border.");
|
||||
} else if (this._gatherModel.selectedSlices.length > 1) {
|
||||
log("Button: Clicked gather to history with too many selections");
|
||||
window.alert("You cannot bring up a history if more than one blue variable name or output has been selected. Make sure only one variable or output is selectedß");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to clear the gathering selections.
|
||||
*/
|
||||
export class ClearButton extends GatherButton {
|
||||
/**
|
||||
* Properties for initializing the clear button.
|
||||
*/
|
||||
readonly CLASS_NAME = "jp-Toolbar-clearbutton";
|
||||
readonly label = "Clear";
|
||||
readonly actionName = "clear-selections";
|
||||
readonly action = {
|
||||
icon: 'fa-remove',
|
||||
help: 'Clear gather selections',
|
||||
help_index: 'clear-selections',
|
||||
handler: () => { this.onClick(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click event
|
||||
*/
|
||||
onClick() {
|
||||
log("Button: Clicked to clear selections", {
|
||||
selectedDefs: this._gatherModel.selectedDefs,
|
||||
selectedOutputs: this._gatherModel.selectedOutputs });
|
||||
this._gatherModel.requestStateChange(GatherState.RESET);
|
||||
}
|
||||
}
|
675
src/nb/index.ts
675
src/nb/index.ts
|
@ -1,675 +0,0 @@
|
|||
import Jupyter = require('base/js/namespace');
|
||||
import * as utils from "base/js/utils";
|
||||
import { Cell, CodeCell, notification_area, Notebook } from 'base/js/namespace';
|
||||
import { Widget } from '@phosphor/widgets';
|
||||
|
||||
import { NotebookCell, copyCodeCell, nbCellToJson } from './NotebookCell';
|
||||
import { ExecutionLogSlicer, SlicedExecution } from '../slicing/ExecutionSlicer';
|
||||
import { MarkerManager, ICell, ICellEditorResolver, ICellOutputResolver, ICellProgramResolver } from '../packages/cell';
|
||||
|
||||
import { GatherModel, GatherState } from '../packages/gather/model';
|
||||
import { GatherController } from '../packages/gather/controller';
|
||||
|
||||
import { GatherToClipboardButton, ClearButton, GatherToNotebookButton, GatherHistoryButton } from './buttons';
|
||||
import { ICellClipboard, IClipboardListener } from '../packages/gather/clipboard';
|
||||
import { INotebookOpener } from '../packages/gather/opener';
|
||||
import * as log from '../utils/log';
|
||||
import { RevisionBrowser } from './RevisionBrowser';
|
||||
|
||||
import 'codemirror/mode/python/python';
|
||||
import '../../style/nb-vars.css';
|
||||
import '../../style/index.css';
|
||||
import { DataflowAnalyzer } from '../slicing/DataflowAnalysis';
|
||||
import { CellProgram } from '../slicing/ProgramBuilder';
|
||||
import { OutputSelection } from '../packages/gather';
|
||||
|
||||
|
||||
/**
|
||||
* Widget for gather notifications.
|
||||
*/
|
||||
var notificationWidget: Jupyter.NotificationWidget;
|
||||
|
||||
/**
|
||||
* Logs cell executions.
|
||||
*/
|
||||
var executionHistory: ExecutionHistory;
|
||||
|
||||
/**
|
||||
* Collects log information about the notebook for each log call.
|
||||
*/
|
||||
class NbStatePoller implements log.IStatePoller {
|
||||
/**
|
||||
* Construct a new poller for notebook state.
|
||||
* Pass in `logCells` as false if you don't want to log information about the cells and their
|
||||
* contents (number, order, total length of text). Collecting the number of cells slows down
|
||||
* execution like crazy because of its internal implementation.
|
||||
*/
|
||||
constructor(notebook: Notebook, logCells?: boolean) {
|
||||
this._notebook = notebook;
|
||||
this._logCells = logCells;
|
||||
// If this notebook doesn't have a UUID, assign one. We'll want to use this to
|
||||
// disambiguate between the notebooks developers are using gathering in.
|
||||
if (!this._notebook.metadata) {
|
||||
this._notebook.metadata = {};
|
||||
}
|
||||
if (!this._notebook.metadata.gatheringId) {
|
||||
// This UUID will stay the same across sessions (i.e. when you reload the notebook),
|
||||
// as long as the notebook was saved after the UUID was assigned.
|
||||
this._notebook.metadata.gatheringId = utils.uuid();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect state information about the notebook.
|
||||
*/
|
||||
poll(): any {
|
||||
let data: any = {
|
||||
gathered: this._notebook.metadata && this._notebook.metadata.gathered,
|
||||
uuid: (this._notebook.metadata ? this._notebook.metadata.gatheringId : undefined),
|
||||
path: this._notebook.notebook_path
|
||||
};
|
||||
if (this._logCells) {
|
||||
let cells = this._notebook.get_cells();
|
||||
data.numCells = cells.length;
|
||||
data.codeCellIds = cells
|
||||
.filter(c => c.cell_type == "code")
|
||||
.map(c => [c.cell_id, (c as CodeCell).input_prompt_number]);
|
||||
data.numLines = cells
|
||||
.filter(c => c.cell_type == "code")
|
||||
.reduce((lineCount, c) => { return lineCount + c.code_mirror.getValue().split("\n").length }, 0)
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private _notebook: Notebook;
|
||||
private _logCells: boolean = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves each cell execution to a history.
|
||||
*/
|
||||
class ExecutionHistory {
|
||||
readonly executionSlicer: ExecutionLogSlicer;
|
||||
private _cellWithUndefinedCount: ICell;
|
||||
private _lastExecutionCount: number;
|
||||
private _gatherModel: GatherModel;
|
||||
|
||||
constructor(notebook: Notebook, gatherModel: GatherModel, dataflowAnalyzer: DataflowAnalyzer) {
|
||||
|
||||
this._gatherModel = gatherModel;
|
||||
this.executionSlicer = new ExecutionLogSlicer(dataflowAnalyzer);
|
||||
|
||||
// We don't know the order that we will receive events for the kernel finishing execution and
|
||||
// a cell finishing execution, so this helps us pair execution count to an executed cell.
|
||||
notebook.events.on('shell_reply.Kernel', (
|
||||
_: Jupyter.Event, data: { reply: { content: Jupyter.ShellReplyContent } }) => {
|
||||
if (this._cellWithUndefinedCount) {
|
||||
console.log("Defining cell execution count after the fact...");
|
||||
this._cellWithUndefinedCount.executionCount = data.reply.content.execution_count;
|
||||
this.executionSlicer.logExecution(this._cellWithUndefinedCount);
|
||||
console.log("Defined from shell_reply");
|
||||
gatherModel.lastExecutedCell = this._cellWithUndefinedCount;
|
||||
this._cellWithUndefinedCount = undefined;
|
||||
} else {
|
||||
this._lastExecutionCount = data.reply.content.execution_count;
|
||||
}
|
||||
});
|
||||
notebook.events.on('finished_execute.CodeCell', (_: Jupyter.Event, data: { cell: CodeCell }) => {
|
||||
let cellClone = copyCodeCell(data.cell);
|
||||
const cell = new NotebookCell(cellClone);
|
||||
if (this._lastExecutionCount) {
|
||||
cellClone.input_prompt_number = this._lastExecutionCount;
|
||||
this.executionSlicer.logExecution(cell);
|
||||
console.log("Defined from finished_execute");
|
||||
gatherModel.lastExecutedCell = cell;
|
||||
this._lastExecutionCount = undefined;
|
||||
} else {
|
||||
this._cellWithUndefinedCount = cell;
|
||||
}
|
||||
});
|
||||
// Clear the history and selections whenever the kernel has been restarted.Z
|
||||
notebook.events.on('kernel_restarting.Kernel', () => {
|
||||
this.executionSlicer.reset();
|
||||
this._gatherModel.clearEditorDefs();
|
||||
this._gatherModel.clearOutputs();
|
||||
this._gatherModel.requestStateChange(GatherState.RESET);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs edit and execution events in the notebook.
|
||||
*/
|
||||
class NotebookEventLogger {
|
||||
/**
|
||||
* Construct a new event logger for the notebook.
|
||||
*/
|
||||
constructor(notebook: Notebook) {
|
||||
// For each of these events, for all cell data that we want to log, make sure to wrap it
|
||||
// first in an `nbCellToJson`---otherwise, logging may crash from circular dependencies, and
|
||||
// we may log data that wasn't intended to be logged.
|
||||
notebook.events.on('create.Cell', (_: Jupyter.Event, data: { cell: Cell, index: number }) => {
|
||||
log.log("Created cell", { cell: nbCellToJson(data.cell), index: data.index });
|
||||
})
|
||||
notebook.events.on('change.Cell', (_: Jupyter.Event, data: { cell: Cell, change: CodeMirror.EditorChange }) => {
|
||||
let change = data.change;
|
||||
// Ignore all `setValue` events---these are invoked programatically, like when a new
|
||||
// cell is created, or when a cell is executed. The other types of events are more
|
||||
// relevant (cut, paste, +input, +delete).
|
||||
if (change.origin != "setValue") {
|
||||
log.log("Changed contents of cell", {
|
||||
cell: nbCellToJson(data.cell),
|
||||
newCharacters: change.text.reduce((len, line) => { return len + line.length }, 0),
|
||||
removedCharacters: change.removed.reduce((len, line) => { return len + line.length }, 0)
|
||||
});
|
||||
}
|
||||
});
|
||||
notebook.events.on('select.Cell', (_: Jupyter.Event, data: { cell: Cell, extendSelection: boolean }) => {
|
||||
log.log("Cell selected", {
|
||||
cell: nbCellToJson(data.cell),
|
||||
extendSelection: data.extendSelection
|
||||
});
|
||||
});
|
||||
notebook.events.on('delete.Cell', (_: Jupyter.Event, data: { cell: Cell, index: number }) => {
|
||||
log.log("Deleted cell", { cell: nbCellToJson(data.cell), index: data.index });
|
||||
});
|
||||
// To my knowledge, the cell that this saves will have the most recent version of the output.
|
||||
notebook.events.on('finished_execute.CodeCell', (_: Jupyter.Event, data: { cell: CodeCell }) => {
|
||||
log.log("Executed cell", { cell: nbCellToJson(data.cell) });
|
||||
});
|
||||
notebook.events.on('checkpoint_created.Notebook', () => {
|
||||
log.log("Created checkpoint");
|
||||
});
|
||||
notebook.events.on('checkpoint_failed.Notebook', () => {
|
||||
log.log("Failed to create checkpoint");
|
||||
});
|
||||
// XXX: Triggered by both restoring a checkpoint and deleting it. Weird.
|
||||
notebook.events.on('notebook_restoring.Notebook', () => {
|
||||
log.log("Attempting to restore checkpoint");
|
||||
});
|
||||
notebook.events.on('checkpoint_restore_failed.Notebook', () => {
|
||||
log.log("Failed to restore checkpoint");
|
||||
});
|
||||
notebook.events.on('checkpoint_restored.Notebook', () => {
|
||||
log.log("Succeeded at restoring checkpoint");
|
||||
});
|
||||
notebook.events.on('checkpoint_delete_failed.Notebook', () => {
|
||||
log.log("Failed to delete checkpoint");
|
||||
});
|
||||
notebook.events.on('checkpoint_deleted.Notebook', () => {
|
||||
log.log("Succeeeded at deleting checkpoint");
|
||||
});
|
||||
// I don't know how a kernel gets killed---its not clear from the notebook interface.
|
||||
notebook.events.on('kernel_killed.Kernel', () => {
|
||||
log.log("Kernel killed");
|
||||
});
|
||||
notebook.events.on('kernel_interrupting.Kernel', () => {
|
||||
log.log("Interrupting the kernel");
|
||||
});
|
||||
notebook.events.on('kernel_restarting.Kernel', () => {
|
||||
log.log("Restarting the kernel");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets cell from a notebook. We use this instead of directly accessing cells on the notebook as
|
||||
* this can speed up cell accesses for costly cell queries.
|
||||
*/
|
||||
class CellFetcher {
|
||||
/**
|
||||
* Construct a new cell fetcher.
|
||||
*/
|
||||
constructor(notebook: Notebook) {
|
||||
this._notebook = notebook;
|
||||
// Invalidate the list of cached cells every time the notebook changes.
|
||||
this._notebook.events.on("set_dirty.Notebook", () => {
|
||||
this._cachedCells = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cell from the notebook with the ID.
|
||||
*/
|
||||
getCellWidgetWithId(cellId: string): Cell {
|
||||
// If the cells haven't been cached, cache 'em here.
|
||||
if (this._cachedCells == null) {
|
||||
this._cachedCells = this._notebook.get_cells();
|
||||
}
|
||||
let matchingCells = this._cachedCells
|
||||
.filter(c => c.cell_id == cellId);
|
||||
if (matchingCells.length > 0) {
|
||||
return matchingCells.pop();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cell from the notebook with the specified properties.
|
||||
*/
|
||||
getCellWidget(cellId: string, executionCount?: number): Cell {
|
||||
let cellWidget = this.getCellWidgetWithId(cellId);
|
||||
if (cellWidget != null && (cellWidget as CodeCell).input_prompt_number == executionCount) {
|
||||
return cellWidget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _notebook: Notebook;
|
||||
private _cachedCells: Cell[] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active editors for cells in Jupyter notebook.
|
||||
* This only works for cells that are still in the notebook---i.e. breaks for deleted cells.
|
||||
*/
|
||||
class NotebookCellEditorResolver implements ICellEditorResolver {
|
||||
/**
|
||||
* Construct a new cell editor resolver.
|
||||
*/
|
||||
constructor(cellFetcher: CellFetcher) {
|
||||
this._cellFetcher = cellFetcher;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): CodeMirror.Editor {
|
||||
let cellWidget = this._cellFetcher.getCellWidgetWithId(cell.id);
|
||||
if (cellWidget) {
|
||||
return cellWidget.code_mirror;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
resolveWithExecutionCount(cell: ICell): CodeMirror.Editor {
|
||||
let cellWidget = this._cellFetcher.getCellWidget(cell.id, cell.executionCount);
|
||||
if (cellWidget) {
|
||||
return cellWidget.code_mirror;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _cellFetcher: CellFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds HTML elements for cell outputs in a notebook.
|
||||
*/
|
||||
class NotebookCellOutputResolver implements ICellOutputResolver {
|
||||
/**
|
||||
* Construct a new cell editor resolver.
|
||||
*/
|
||||
constructor(cellFetcher: CellFetcher) {
|
||||
this._cellFetcher = cellFetcher;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): HTMLElement[] {
|
||||
let cellWidget = this._cellFetcher.getCellWidgetWithId(cell.id);
|
||||
let outputElements = [];
|
||||
if (cellWidget) {
|
||||
let cellElement = cellWidget.element[0];
|
||||
var outputNodes = cellElement.querySelectorAll(".output_subarea");
|
||||
for (var i = 0; i < outputNodes.length; i++) {
|
||||
if (outputNodes[i] instanceof HTMLElement) {
|
||||
outputElements.push(outputNodes[i] as HTMLElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputElements;
|
||||
}
|
||||
|
||||
private _cellFetcher: CellFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps cells to the code analysis information.
|
||||
*/
|
||||
class CellProgramResolver implements ICellProgramResolver {
|
||||
/**
|
||||
* Construct a new cell program resolver
|
||||
*/
|
||||
constructor(executionLogSlicer: ExecutionLogSlicer) {
|
||||
this._executionLogSlicer = executionLogSlicer;
|
||||
}
|
||||
|
||||
resolve(cell: ICell): CellProgram {
|
||||
return this._executionLogSlicer.getCellProgram(cell);
|
||||
}
|
||||
|
||||
private _executionLogSlicer: ExecutionLogSlicer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights gatherable entities.
|
||||
*/
|
||||
class ResultsHighlighter {
|
||||
|
||||
private _markerManager: MarkerManager;
|
||||
|
||||
constructor(notebook: Notebook, gatherModel: GatherModel, markerManager: MarkerManager) {
|
||||
this._markerManager = markerManager;
|
||||
|
||||
// Event listener for execution is in execution history, as we need to parse and
|
||||
// detect defs in a cell before updating the markers.
|
||||
notebook.events.on('change.Cell', (_: Jupyter.Event, data: { cell: Cell, change: CodeMirror.EditorChange }) => {
|
||||
let change = data.change;
|
||||
// Ignore all `setValue` events---these are invoked programatically.
|
||||
if (change.origin != "setValue" && data.cell instanceof CodeCell) {
|
||||
gatherModel.lastEditedCell = new NotebookCell(data.cell);
|
||||
}
|
||||
});
|
||||
notebook.events.on('delete.Cell', (_: Jupyter.Event, data: { cell: Cell, index: number }) => {
|
||||
if (data.cell instanceof CodeCell) {
|
||||
gatherModel.lastDeletedCell = new NotebookCell(data.cell);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("mouseup", (event: MouseEvent) => {
|
||||
this._markerManager.handleClick(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert program slice to list of cell JSONs
|
||||
*/
|
||||
function sliceToCellJson(slice: SlicedExecution, outputSelections?: OutputSelection[],
|
||||
annotatePaste?: boolean): CellJson[] {
|
||||
|
||||
const SHOULD_SLICE_CELLS = true;
|
||||
const OMIT_UNSELECTED_OUTPUT = true;
|
||||
|
||||
outputSelections = outputSelections || [];
|
||||
annotatePaste = annotatePaste || false;
|
||||
|
||||
return slice.cellSlices
|
||||
.map((cellSlice, i) => {
|
||||
let slicedCell = cellSlice.cell;
|
||||
if (SHOULD_SLICE_CELLS) {
|
||||
slicedCell = slicedCell.copy();
|
||||
slicedCell.text = cellSlice.textSliceLines;
|
||||
}
|
||||
if (slicedCell instanceof NotebookCell) {
|
||||
let cellJson = slicedCell.model.toJSON();
|
||||
// This new cell hasn't been executed yet. So don't mark it as having been executed.
|
||||
cellJson.execution_count = null;
|
||||
// Add a flag to distinguish gathered cells from other cells.
|
||||
cellJson.metadata.gathered = true;
|
||||
// Add a flag so we can tell if this cell was just pasted, so we can merge it.
|
||||
if (annotatePaste) {
|
||||
cellJson.metadata.justPasted = true;
|
||||
}
|
||||
// Filter to just those outputs that were selected.
|
||||
if (OMIT_UNSELECTED_OUTPUT) {
|
||||
let originalOutputs = cellJson.outputs;
|
||||
cellJson.outputs = [];
|
||||
for (let i = 0; i < originalOutputs.length; i++) {
|
||||
let output = originalOutputs[i];
|
||||
if (outputSelections.some(s => s.cell.id == slicedCell.id && s.outputIndex == i)) {
|
||||
cellJson.outputs.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellJson;
|
||||
}
|
||||
}).filter(c => c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather code to the clipboard.
|
||||
*/
|
||||
class Clipboard implements ICellClipboard {
|
||||
|
||||
constructor(gatherModel: GatherModel) {
|
||||
this._gatherModel = gatherModel;
|
||||
}
|
||||
|
||||
addListener(listener: IClipboardListener) {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
|
||||
copy(slice: SlicedExecution) {
|
||||
if (slice) {
|
||||
// Copy to the Jupyter internal clipboard
|
||||
Jupyter.notebook.clipboard = [];
|
||||
let cellsJson = sliceToCellJson(slice, this._gatherModel.selectedOutputs.concat(), true);
|
||||
cellsJson.forEach(c => {
|
||||
Jupyter.notebook.clipboard.push(c);
|
||||
});
|
||||
Jupyter.notebook.enable_paste();
|
||||
this._listeners.forEach(listener => listener.onCopy(slice, this));
|
||||
|
||||
// Also copy the text to the browser's clipboard, so it can be pasted into a cell.
|
||||
// XXX: attach an invisible textarea to the page, and add the slice text to it, so we
|
||||
// can use a cross-browser command for copying to the clipboard.
|
||||
let fullSliceText = slice.cellSlices.map((cs) => cs.textSliceLines).join("\n\n");
|
||||
let textarea = document.createElement('textarea');
|
||||
textarea.style.top = "0px";
|
||||
textarea.style.left = "0px";
|
||||
textarea.style.width = "2em";
|
||||
textarea.style.height = "2em";
|
||||
textarea.style.border = "none";
|
||||
textarea.style.background = "transparent";
|
||||
textarea.value = fullSliceText;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
private _listeners: IClipboardListener[] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new notebooks containing program slices.
|
||||
*/
|
||||
class NotebookOpener implements INotebookOpener {
|
||||
|
||||
// Pass in the current notebook. This class will open new notebooks.
|
||||
constructor(thisNotebook: Notebook, gatherModel: GatherModel) {
|
||||
this._notebook = thisNotebook;
|
||||
this._gatherModel = gatherModel;
|
||||
}
|
||||
|
||||
private _openSlice(notebookJson: NotebookJson, gatherIndex: number) {
|
||||
|
||||
// Get the directory of the current notebook.
|
||||
let currentDir = document.body.attributes
|
||||
.getNamedItem('data-notebook-path').value.split('/').slice(0, -1).join("/");
|
||||
currentDir = currentDir ? currentDir + "/" : "";
|
||||
|
||||
// Create path to file
|
||||
let fileName = "GatheredCode" + gatherIndex + ".ipynb";
|
||||
let notebookPath = currentDir + fileName;
|
||||
|
||||
this._notebook.contents.get(notebookPath, { type: 'notebook' }).then((_) => {
|
||||
// If there's already a file at this location, try the next gather index.
|
||||
this._openSlice(notebookJson, gatherIndex + 1);
|
||||
}, (_) => {
|
||||
// Open up a new notebook at an available location.
|
||||
let model = { type: "notebook", content: notebookJson };
|
||||
this._notebook.contents.save(notebookPath, model).then(() => {
|
||||
// XXX: This seems to open up a file in different places on different machines???
|
||||
let nbWindow = window.open(fileName + "?kernel_name=python3", '_blank');
|
||||
if (nbWindow == null) {
|
||||
window.alert("Please allow popups for Jupyter notebook.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
openNotebookForSlice(slice: SlicedExecution) {
|
||||
|
||||
// Make boilerplate, empty notebook JSON.
|
||||
let notebookJson = this._notebook.toJSON();
|
||||
notebookJson.cells = [];
|
||||
notebookJson.metadata.gathered = true;
|
||||
|
||||
// Replace the notebook model's cells with the copied cells.
|
||||
if (slice) {
|
||||
let cellsJson = sliceToCellJson(slice, this._gatherModel.selectedOutputs.concat(), false);
|
||||
for (let i = 0; i < cellsJson.length; i++) {
|
||||
let cellJson = cellsJson[i];
|
||||
notebookJson.cells.push(cellJson);
|
||||
}
|
||||
|
||||
// Save the gathered code to a new notebook, and then open it.
|
||||
this._openSlice(notebookJson, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _gatherModel: GatherModel;
|
||||
private _notebook: Notebook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix for all gather actions.
|
||||
*/
|
||||
const GATHER_PREFIX = 'gather_extension';
|
||||
|
||||
type UrlOptions = {
|
||||
autoExecute: boolean,
|
||||
gatheringDisabled: boolean
|
||||
};
|
||||
|
||||
function getUrlOptions(window: Window): UrlOptions {
|
||||
let url = window.location.href;
|
||||
function getOption(url: string, name: string): string {
|
||||
let match = url.match(new RegExp("(?:&|\\?)" + name + "=([^$&]*)"));
|
||||
if (match == null || !match.length || match.length <= 1) return null;
|
||||
return match[1];
|
||||
}
|
||||
return {
|
||||
autoExecute: getOption(url, "autoExecute") == "true",
|
||||
gatheringDisabled: getOption(url, "gatheringDisabled") == "true"
|
||||
}
|
||||
}
|
||||
|
||||
export function load_ipython_extension() {
|
||||
console.log('extension started');
|
||||
|
||||
// Get options for the plugin from the URL
|
||||
let options = getUrlOptions(window);
|
||||
|
||||
// Exit early if gathering is disabled.
|
||||
if (options.gatheringDisabled) return;
|
||||
|
||||
// If the notebook is set to autoExecute, find all cells that have been executed before and
|
||||
// execute them again.
|
||||
if (options.autoExecute) {
|
||||
let cells = Jupyter.notebook.get_cells();
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
let cell = cells[i];
|
||||
if (cell.cell_type == "code") {
|
||||
let codeCell = cell as CodeCell;
|
||||
if (codeCell.input_prompt_number) {
|
||||
// When executing, don't stop the kernel on error---keep running the other
|
||||
// cells, even if some of them throw an exception, so we can replicate
|
||||
// those exceptions.
|
||||
codeCell.execute(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
const LOG_NB_CELLS = false;
|
||||
log.initLogger({ ajax: utils.ajax });
|
||||
log.registerPollers(new NbStatePoller(Jupyter.notebook, LOG_NB_CELLS));
|
||||
new NotebookEventLogger(Jupyter.notebook);
|
||||
|
||||
// Object containing global UI state.
|
||||
let gatherModel = new GatherModel();
|
||||
|
||||
// Shared dataflow analysis object.
|
||||
let dataflowAnalyzer = new DataflowAnalyzer();
|
||||
|
||||
// Plugin initializations.
|
||||
executionHistory = new ExecutionHistory(Jupyter.notebook, gatherModel, dataflowAnalyzer);
|
||||
let cellFetcher = new CellFetcher(Jupyter.notebook);
|
||||
let markerManager = new MarkerManager(gatherModel,
|
||||
new CellProgramResolver(executionHistory.executionSlicer),
|
||||
new NotebookCellEditorResolver(cellFetcher),
|
||||
new NotebookCellOutputResolver(cellFetcher));
|
||||
new ResultsHighlighter(Jupyter.notebook, gatherModel, markerManager);
|
||||
|
||||
// Initialize clipboard for copying cells.
|
||||
let clipboard = new Clipboard(gatherModel);
|
||||
clipboard.addListener({
|
||||
onCopy: () => {
|
||||
if (notificationWidget) {
|
||||
notificationWidget.set_message("Copied cells to clipboard.", 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize utility for opening new notebooks.
|
||||
let opener = new NotebookOpener(Jupyter.notebook, gatherModel);
|
||||
|
||||
// Controller for global UI state.
|
||||
new GatherController(gatherModel, executionHistory.executionSlicer, clipboard, opener);
|
||||
|
||||
// Set up toolbar with gather actions.
|
||||
let gatherToClipboardButton = new GatherToClipboardButton(gatherModel);
|
||||
let gatherToNotebookButton = new GatherToNotebookButton(gatherModel);
|
||||
let gatherHistoryButton = new GatherHistoryButton(gatherModel);
|
||||
let clearButton = new ClearButton(gatherModel);
|
||||
|
||||
// Create buttons for gathering.
|
||||
let buttonsGroup = Jupyter.toolbar.add_buttons_group(
|
||||
[gatherToClipboardButton, gatherToNotebookButton, gatherHistoryButton, clearButton]
|
||||
.map(b => ({
|
||||
label: b.label,
|
||||
icon: b.action.icon,
|
||||
callback: b.action.handler,
|
||||
action: Jupyter.actions.register(b.action, b.actionName, GATHER_PREFIX)
|
||||
}))
|
||||
);
|
||||
|
||||
// Add a label to the gathering part of the toolbar.
|
||||
let gatherLabel = document.createElement("div");
|
||||
gatherLabel.textContent = "Gather to:";
|
||||
gatherLabel.classList.add("jp-Toolbar-gatherlabel");
|
||||
buttonsGroup[0].insertBefore(gatherLabel, buttonsGroup.children()[0]);
|
||||
|
||||
// Finish initializing the buttons.
|
||||
gatherToClipboardButton.node = new Widget({ node: buttonsGroup.children()[1] });
|
||||
gatherToNotebookButton.node = new Widget({ node: buttonsGroup.children()[2] });
|
||||
gatherHistoryButton.node = new Widget({ node: buttonsGroup.children()[3] });
|
||||
clearButton.node = new Widget({ node: buttonsGroup.children()[4] });
|
||||
|
||||
// Add widget for viewing history
|
||||
let revisionBrowser = new RevisionBrowser(gatherModel);
|
||||
document.body.appendChild(revisionBrowser.node);
|
||||
|
||||
// When pasting gathered cells, select those cells. This is hacky: we add a flag to the
|
||||
// gathered cells (justPasted) so we can find them right after the paste, as there is no
|
||||
// listener for pasting gathered cells in the notebook API.
|
||||
Jupyter.notebook.events.on('select.Cell', (_: Jupyter.Event, data: { cell: Cell }) => {
|
||||
|
||||
let cell = data.cell;
|
||||
if (cell.metadata.justPasted) {
|
||||
|
||||
// Select all of the gathered cells.
|
||||
let gatheredCellIndexes = cell.notebook.get_cells()
|
||||
.map((c, i): [Cell, number] => [c, i])
|
||||
.filter(([c, i]) => c.metadata.justPasted)
|
||||
.map(([c, i]) => i);
|
||||
let firstGatheredIndex = Math.min(...gatheredCellIndexes);
|
||||
let lastGatheredIndex = Math.max(...gatheredCellIndexes);
|
||||
Jupyter.notebook.select(firstGatheredIndex, true);
|
||||
Jupyter.notebook.select(lastGatheredIndex, false);
|
||||
|
||||
// We won't use the `gathered` flag on these cells anymore, so remove them from the cells.
|
||||
cell.notebook.get_cells()
|
||||
.forEach(c => {
|
||||
if (c.metadata.justPasted) {
|
||||
delete c.metadata.justPasted;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
notificationWidget = notification_area.new_notification_widget("gather");
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
__pycache__
|
||||
*.egg-info
|
||||
venv
|
|
@ -1,41 +0,0 @@
|
|||
"""
|
||||
Based on the tutorial at:
|
||||
https://jupyter-notebook.readthedocs.io/en/latest/extending/handlers.html
|
||||
"""
|
||||
|
||||
from notebook.utils import url_path_join
|
||||
from notebook.base.handlers import IPythonHandler
|
||||
from tornado.web import MissingArgumentError
|
||||
import portalocker
|
||||
import os.path
|
||||
|
||||
|
||||
# Initialize the log directory
|
||||
LOG_DIR = os.path.join(os.path.expanduser("~"), ".jupyter")
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
LOG_PATH = os.path.join(LOG_DIR, "log.txt")
|
||||
|
||||
|
||||
def _jupyter_server_extension_paths():
|
||||
return [{
|
||||
"module": "gather_logger"
|
||||
}]
|
||||
|
||||
|
||||
def load_jupyter_server_extension(nb_server_app):
|
||||
nb_server_app.log.info("Starting the Gathering Logger extension")
|
||||
web_app = nb_server_app.web_app
|
||||
host_pattern = '.*$'
|
||||
route_pattern = url_path_join(web_app.settings['base_url'], '/log')
|
||||
web_app.add_handlers(host_pattern, [(route_pattern, LogHandler)])
|
||||
nb_server_app.log.info("Successfully started the Gathering Logger extension")
|
||||
|
||||
|
||||
class LogHandler(IPythonHandler):
|
||||
|
||||
def post(self):
|
||||
data = self.request.body.decode('utf-8')
|
||||
with portalocker.Lock(LOG_PATH, mode='a', timeout=1) as fh:
|
||||
fh.write(data + "\n")
|
||||
self.write({ "result": "OK" })
|
|
@ -1,7 +0,0 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(name='gather-logger',
|
||||
version='0.0',
|
||||
py_modules=['gather_logger/__init__'],
|
||||
install_requires=['portalocker'],
|
||||
)
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
This is an attempt to get minimal coverage of Jupyter Notebook's internal API
|
||||
for writing our extension. We welcome contributions to flesh this out more!
|
||||
*/
|
||||
declare namespace Jupyter {
|
||||
|
||||
interface Notebook {
|
||||
base_url: string;
|
||||
get_cells(): Cell[];
|
||||
get_selected_cell(): Cell;
|
||||
get_selected_cells(): Cell[];
|
||||
get_selected_cells_indices(): number[];
|
||||
select: (index: number, moveanchor: boolean) => Notebook;
|
||||
events: Events;
|
||||
contents: Contents;
|
||||
config: Config;
|
||||
clipboard: Array<any>;
|
||||
enable_paste: () => void;
|
||||
paste_enabled: boolean;
|
||||
notebook_path: string;
|
||||
execute_cells: (indices: number[]) => void;
|
||||
toJSON: () => NotebookJson;
|
||||
metadata: NotebookMetadata;
|
||||
}
|
||||
|
||||
interface Dialog {
|
||||
modal(spec: { title: string, body: any, buttons: any }): void;
|
||||
}
|
||||
|
||||
interface CommandShortcuts {
|
||||
add_shortcut(shortcut: string, callback: () => void): void;
|
||||
}
|
||||
|
||||
interface KeyboardManager {
|
||||
command_shortcuts: CommandShortcuts;
|
||||
}
|
||||
|
||||
interface Event {
|
||||
namespace: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Cell {
|
||||
cell_id: string;
|
||||
cell_type: 'code' | 'markdown';
|
||||
config: Config;
|
||||
element: JQuery;
|
||||
notebook: Notebook;
|
||||
metadata: CellMetadata;
|
||||
code_mirror: CodeMirror.Editor;
|
||||
events: Events;
|
||||
keyboard_manager: KeyboardManager;
|
||||
}
|
||||
interface CellConstructor {
|
||||
new(options: any): Cell;
|
||||
}
|
||||
var Cell: CellConstructor;
|
||||
|
||||
interface Output {
|
||||
output_type: string;
|
||||
data: { [mimeType: string]: any }
|
||||
}
|
||||
|
||||
interface OutputArea {
|
||||
outputs: Output[];
|
||||
element: JQuery;
|
||||
}
|
||||
interface OutputAreaConstructor {
|
||||
new(options: any): OutputArea;
|
||||
}
|
||||
var OutputArea: OutputAreaConstructor;
|
||||
|
||||
interface CodeCell extends Cell {
|
||||
cell_type: 'code';
|
||||
input_prompt_number: number;
|
||||
output_area: OutputArea;
|
||||
kernel: Kernel;
|
||||
tooltip: Tooltip;
|
||||
execute: (stop_on_error: boolean) => void;
|
||||
fromJSON: (data: CellJson) => void;
|
||||
toJSON: () => CellJson;
|
||||
}
|
||||
interface CodeCellConstructor {
|
||||
new(kernel: Kernel, options: CodeCellOptions): CodeCell;
|
||||
}
|
||||
var CodeCell: CodeCellConstructor;
|
||||
|
||||
interface Kernel { }
|
||||
|
||||
interface Tooltip { }
|
||||
|
||||
interface Config { }
|
||||
|
||||
interface CodeCellOptions {
|
||||
events: Events,
|
||||
config: Config,
|
||||
keyboard_manager: KeyboardManager,
|
||||
notebook: Notebook,
|
||||
tooltip: Tooltip
|
||||
}
|
||||
|
||||
interface Events {
|
||||
on(name: string, callback: (evt: any, data: any) => void): void;
|
||||
trigger(name: string, data: any): void;
|
||||
}
|
||||
|
||||
interface Contents {
|
||||
new_untitled(path: string, options: { ext?: string, type?: string }): Promise<{ path: string }>;
|
||||
save(path: string, model: SaveModel): Promise<any>;
|
||||
get(path: string, data: { type: string, content?: boolean }): Promise<any>;
|
||||
}
|
||||
|
||||
interface SaveModel {
|
||||
type: string;
|
||||
content: NotebookJson;
|
||||
}
|
||||
|
||||
interface ShellReplyContent {
|
||||
execution_count: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface NotificationWidget {
|
||||
set_message: (message: string, timeMs?: number) => void;
|
||||
}
|
||||
|
||||
interface NotificationArea {
|
||||
new_notification_widget: (name: string) => NotificationWidget;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
register: (action: Action, action_name: string, prefix: string) => string;
|
||||
call: (actionName: string) => void;
|
||||
}
|
||||
|
||||
interface Action {
|
||||
icon: string; // font-awesome class
|
||||
help: string;
|
||||
help_index: string;
|
||||
handler: () => void;
|
||||
}
|
||||
|
||||
interface ActionSpec {
|
||||
label: string;
|
||||
icon: string;
|
||||
callback: () => void
|
||||
action: string; // action name
|
||||
}
|
||||
|
||||
interface Toolbar {
|
||||
add_buttons_group: (actions: ActionSpec[]) => JQuery;
|
||||
}
|
||||
|
||||
var actions: Actions;
|
||||
var contents: Contents;
|
||||
var dialog: Dialog;
|
||||
var keyboard_manager: KeyboardManager;
|
||||
var notebook: Notebook;
|
||||
var notification_area: NotificationArea;
|
||||
var toolbar: Toolbar;
|
||||
}
|
||||
|
||||
// This is not from base/ns/namespace. We declared it so we can type-check the output of the
|
||||
// toJSON method on notebooks.
|
||||
declare interface NotebookJson {
|
||||
cells: CellJson[];
|
||||
metadata: NotebookJsonMetadata;
|
||||
}
|
||||
|
||||
declare interface NotebookMetadata {
|
||||
gathered?: boolean;
|
||||
gatheringId?: string;
|
||||
}
|
||||
|
||||
declare interface CellMetadata {
|
||||
gathered?: boolean;
|
||||
justPasted?: boolean;
|
||||
}
|
||||
|
||||
declare interface NotebookJsonMetadata {
|
||||
gathered?: boolean;
|
||||
}
|
||||
|
||||
declare interface CellJson {
|
||||
source: string;
|
||||
outputs: JSON[];
|
||||
cell_type: string;
|
||||
execution_count: number;
|
||||
metadata: CellJsonMetadata;
|
||||
}
|
||||
|
||||
declare interface CellJsonMetadata {
|
||||
gathered?: boolean;
|
||||
justPasted?: boolean;
|
||||
}
|
||||
|
||||
// declare const Jupyter: Jupyter.JupyterStatic;
|
||||
|
||||
declare module "base/js/namespace" {
|
||||
export = Jupyter;
|
||||
}
|
||||
|
||||
declare namespace Utils {
|
||||
function ajax(url: string | any, settings: any): XMLHttpRequest;
|
||||
function uuid(): string;
|
||||
}
|
||||
|
||||
declare module "base/js/utils" {
|
||||
export = Utils;
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
/**
|
||||
* Helpers for marking up CodeMirror editors.
|
||||
*/
|
||||
import { ISyntaxNode, ILocation } from "../../parsers/python/python_parser";
|
||||
import { SymbolType, Ref } from "../../slicing/DataflowAnalysis";
|
||||
import { GatherModel, IGatherObserver, GatherEventData, GatherModelEvent, EditorDef, DefSelection, OutputSelection, CellOutput } from "../gather";
|
||||
import { ICell } from "./model";
|
||||
import { NotebookPanel } from "@jupyterlab/notebook";
|
||||
import { LineHandle } from "../../../node_modules/@types/codemirror";
|
||||
import { NotebookElementFinder } from "../../lab/element-finder";
|
||||
import { ILocation, ISyntaxNode } from "../../parsers/python/python_parser";
|
||||
import { Ref, SymbolType } from "../../slicing/DataflowAnalysis";
|
||||
import { SlicedExecution } from "../../slicing/ExecutionSlicer";
|
||||
import { log } from "../../utils/log";
|
||||
import { LineHandle } from "../../../node_modules/@types/codemirror";
|
||||
import { CellProgram } from "../../slicing/ProgramBuilder";
|
||||
import { CellOutput, DefSelection, EditorDef, GatherEventData, GatherModel, GatherModelEvent, IGatherObserver, OutputSelection } from "../gather";
|
||||
import { ICell } from "./model";
|
||||
|
||||
/**
|
||||
* Class for a highlighted, clickable output.
|
||||
|
@ -51,72 +52,35 @@ function clearSelectionsInWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves cells to active editors in the notebook.
|
||||
* Necessary because most of the cell data passed around the notebook are clones with editors
|
||||
* that aren't actually active on the page.
|
||||
*/
|
||||
export interface ICellEditorResolver {
|
||||
/**
|
||||
* Get the active CodeMirror editor for this cell.
|
||||
*/
|
||||
resolve(cell: ICell): CodeMirror.Editor;
|
||||
|
||||
/**
|
||||
* Additionally filter to make sure the execution count matches.
|
||||
* (Don't call this right after a cell execution event, as it takes a while for the
|
||||
* execution count to update in an executed cell).
|
||||
*/
|
||||
resolveWithExecutionCount(cell: ICell): CodeMirror.Editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves cells to their program information (parse tree, definitions, etc.)
|
||||
* This makes sure we don't have to do any parsing / dataflow analysis within
|
||||
* this display module.
|
||||
*/
|
||||
export interface ICellProgramResolver {
|
||||
resolve(cell: ICell): CellProgram;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves cells to the HTML elements for their outputs.
|
||||
*/
|
||||
export interface ICellOutputResolver {
|
||||
/**
|
||||
* Get the divs containing output for this cell.
|
||||
* Currently, we recommend implementations don't pay attention to the execution
|
||||
* count, but just get the outputs for a cell with an ID.
|
||||
*/
|
||||
resolve(cell: ICell): HTMLElement[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and manages text markers.
|
||||
*/
|
||||
export class MarkerManager implements IGatherObserver {
|
||||
/**
|
||||
* Construct a new marker manager.
|
||||
*/
|
||||
constructor(model: GatherModel, cellProgramResolver: ICellProgramResolver,
|
||||
cellEditorResolver: ICellEditorResolver,
|
||||
cellOutputResolver: ICellOutputResolver) {
|
||||
this._model = model;
|
||||
this._model.addObserver(this);
|
||||
this._cellProgramResolver = cellProgramResolver;
|
||||
this._cellEditorResolver = cellEditorResolver;
|
||||
this._cellOutputResolver = cellOutputResolver;
|
||||
}
|
||||
|
||||
private _model: GatherModel;
|
||||
private _cellProgramResolver: ICellProgramResolver;
|
||||
private _cellEditorResolver: ICellEditorResolver;
|
||||
private _cellOutputResolver: ICellOutputResolver;
|
||||
private _elementFinder: NotebookElementFinder;
|
||||
private _defMarkers: DefMarker[] = [];
|
||||
private _defLineHandles: DefLineHandle[] = [];
|
||||
private _outputMarkers: OutputMarker[] = [];
|
||||
private _dependencyLineMarkers: DependencyLineMarker[] = [];
|
||||
|
||||
/**
|
||||
* Construct a new marker manager.
|
||||
*/
|
||||
constructor(model: GatherModel, notebook: NotebookPanel) {
|
||||
this._model = model;
|
||||
this._model.addObserver(this);
|
||||
this._elementFinder = new NotebookElementFinder(notebook);
|
||||
|
||||
/*
|
||||
* XXX(andrewhead): Sometimes in Chrome or Edge, "click" events get dropped when the click
|
||||
* occurs on the cell. Mouseup doesn't, so we use that here.
|
||||
*/
|
||||
notebook.content.node.addEventListener("mouseup", (event: MouseEvent) => {
|
||||
this.handleClick(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click-handler---pass on click event to markers.
|
||||
*/
|
||||
|
@ -135,11 +99,11 @@ export class MarkerManager implements IGatherObserver {
|
|||
if (eventType == GatherModelEvent.CELL_EXECUTED) {
|
||||
let cell = eventData as ICell;
|
||||
this.clearSelectablesForCell(cell);
|
||||
let editor = this._cellEditorResolver.resolve(cell);
|
||||
let editor = this._elementFinder.getEditor(cell);
|
||||
if (editor) {
|
||||
this.highlightDefs(editor, cell);
|
||||
}
|
||||
let outputElements = this._cellOutputResolver.resolve(cell);
|
||||
let outputElements = this._elementFinder.getOutputs(cell);
|
||||
this.highlightOutputs(cell, outputElements);
|
||||
}
|
||||
|
||||
|
@ -206,7 +170,7 @@ export class MarkerManager implements IGatherObserver {
|
|||
let defSelection = eventData as DefSelection;
|
||||
this._defMarkers.filter(marker => {
|
||||
return defSelection.editorDef.def.location == marker.location &&
|
||||
defSelection.cell.id == marker.cell.id;
|
||||
defSelection.cell.persistentId == marker.cell.persistentId;
|
||||
}).forEach(marker => marker.deselect());
|
||||
|
||||
let editorDef = defSelection.editorDef;
|
||||
|
@ -224,7 +188,7 @@ export class MarkerManager implements IGatherObserver {
|
|||
let outputSelection = eventData as OutputSelection;
|
||||
this._outputMarkers.filter(marker => {
|
||||
return marker.outputIndex == outputSelection.outputIndex &&
|
||||
marker.cell.id == outputSelection.cell.id;
|
||||
marker.cell.persistentId == outputSelection.cell.persistentId;
|
||||
}).forEach(marker => marker.deselect());
|
||||
}
|
||||
|
||||
|
@ -290,15 +254,19 @@ export class MarkerManager implements IGatherObserver {
|
|||
* Clear all def markers that belong to this editor.
|
||||
*/
|
||||
clearSelectablesForCell(cell: ICell) {
|
||||
this._model.removeEditorDefsForCell(cell.id);
|
||||
this._model.deselectOutputsForCell(cell.id);
|
||||
this._model.removeEditorDefsForCell(cell.persistentId);
|
||||
this._model.deselectOutputsForCell(cell.persistentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight all of the definitions in an editor.
|
||||
*/
|
||||
highlightDefs(editor: CodeMirror.Editor, cell: ICell) {
|
||||
let cellProgram = this._cellProgramResolver.resolve(cell);
|
||||
/**
|
||||
* Fetch the cell program instead of recomputing it, as it can stall the interface if we
|
||||
* analyze the code here.
|
||||
*/
|
||||
let cellProgram = this._model.getCellProgram(cell);
|
||||
if (!cellProgram.hasError) {
|
||||
for (let ref of cellProgram.defs) {
|
||||
if (ref.type == SymbolType.VARIABLE) {
|
||||
|
@ -329,7 +297,7 @@ export class MarkerManager implements IGatherObserver {
|
|||
slice.cellSlices.forEach(cellSlice => {
|
||||
let cell = cellSlice.cell;
|
||||
let sliceLocations = cellSlice.slice;
|
||||
let editor = this._cellEditorResolver.resolveWithExecutionCount(cell);
|
||||
let editor = this._elementFinder.getEditorWithExecutionCount(cell);
|
||||
|
||||
if (editor) {
|
||||
let numLines = 0;
|
||||
|
|
|
@ -6,12 +6,22 @@ import { LocationSet } from "../../slicing/Slice";
|
|||
export interface ICell {
|
||||
is_cell: boolean;
|
||||
id: string;
|
||||
/**
|
||||
* TODO(andrewhead): make sure that when a cell is copied, it doesn't have the same
|
||||
* persistent ID.
|
||||
*/
|
||||
persistentId: string;
|
||||
executionCount: number;
|
||||
hasError: boolean;
|
||||
isCode: boolean;
|
||||
text: string;
|
||||
gathered: boolean;
|
||||
copy: () => ICell // deep copy if holding a model.
|
||||
copy: () => ICell; // deep copy if holding a model.
|
||||
/**
|
||||
* Produce JSON that can be read to make a new Jupyter notebook cell.
|
||||
* TODO(andrewhead): return JSON with cell JSON static type.
|
||||
*/
|
||||
toJupyterJSON: () => any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,6 +31,7 @@ export abstract class AbstractCell implements ICell {
|
|||
|
||||
abstract is_cell: boolean;
|
||||
abstract id: string;
|
||||
abstract persistentId: string;
|
||||
abstract executionCount: number;
|
||||
abstract hasError: boolean;
|
||||
abstract isCode: boolean;
|
||||
|
@ -29,22 +40,65 @@ export abstract class AbstractCell implements ICell {
|
|||
abstract copy(): AbstractCell;
|
||||
|
||||
/**
|
||||
* Output descriptive (unsensitive) data about this cell.
|
||||
* Output descriptive (unsensitive) data about this cell. No code!
|
||||
*/
|
||||
toJSON(): any {
|
||||
return {
|
||||
id: this.id,
|
||||
persistentId: this.persistentId,
|
||||
executionCount: this.executionCount,
|
||||
lineCount: this.text.split("\n").length,
|
||||
isCode: this.isCode,
|
||||
hasError: this.hasError,
|
||||
gathered: this.gathered
|
||||
gathered: this.gathered,
|
||||
};
|
||||
}
|
||||
|
||||
toJupyterJSON(): any {
|
||||
return {
|
||||
id: this.id,
|
||||
execution_count: this.executionCount,
|
||||
source: this.text,
|
||||
cell_type: "code",
|
||||
metadata: {
|
||||
gathered: this.gathered,
|
||||
persistent_id: this.persistentId,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleCell extends AbstractCell {
|
||||
|
||||
constructor(id: string, persistentId: string, executionCount: number,
|
||||
hasError: boolean, isCode: boolean, text: string) {
|
||||
super();
|
||||
this.is_cell = true;
|
||||
this.id = id;
|
||||
this.persistentId = persistentId;
|
||||
this.executionCount = executionCount;
|
||||
this.hasError = hasError;
|
||||
this.isCode = isCode;
|
||||
this.text = text;
|
||||
this.gathered = false;
|
||||
}
|
||||
|
||||
copy(): SimpleCell {
|
||||
return new SimpleCell(this.id, this.persistentId, this.executionCount, this.hasError, this.isCode, this.text);
|
||||
}
|
||||
|
||||
public readonly is_cell: boolean;
|
||||
public readonly id: string;
|
||||
public readonly persistentId: string;
|
||||
public readonly executionCount: number;
|
||||
public readonly hasError: boolean;
|
||||
public readonly isCode: boolean;
|
||||
public readonly text: string;
|
||||
public readonly gathered: boolean;
|
||||
}
|
||||
|
||||
export function instanceOfICell(object: any): object is ICell {
|
||||
return object && typeof(object) == "object" && "is_cell" in object;
|
||||
return object && (typeof object == "object") && "is_cell" in object;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,14 +113,14 @@ export interface IOutputterCell<TOutputModel> extends ICell {
|
|||
* Type checker for IOutputterCell.
|
||||
*/
|
||||
export function instanceOfIOutputterCell<TOutputModel>(object: any): object is IOutputterCell<TOutputModel> {
|
||||
return object && typeof(object) == "object" && "is_outputter_cell" in object;
|
||||
return object && (typeof object == "object") && "is_outputter_cell" in object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for a cell with output data.
|
||||
*/
|
||||
export abstract class AbstractOutputterCell<TOutputModel>
|
||||
extends AbstractCell implements IOutputterCell<TOutputModel> {
|
||||
extends AbstractCell implements IOutputterCell<TOutputModel> {
|
||||
|
||||
readonly is_outputter_cell: boolean = true;
|
||||
abstract output: TOutputModel;
|
||||
|
@ -104,32 +158,32 @@ export class CellSlice {
|
|||
let sliceLocations = this.slice.items;
|
||||
let textLines = this.cell.text.split("\n");
|
||||
return sliceLocations.sort((l1, l2) => l1.first_line - l2.first_line)
|
||||
.map(loc => {
|
||||
return textLines.map((line, index0) => {
|
||||
let index = index0 + 1;
|
||||
let left, right;
|
||||
if (index == loc.first_line) {
|
||||
left = loc.first_column;
|
||||
}
|
||||
if (index == loc.last_line) {
|
||||
right = loc.last_column;
|
||||
}
|
||||
if (index > loc.first_line) {
|
||||
left = 0;
|
||||
}
|
||||
if (index < loc.last_line) {
|
||||
right = line.length;
|
||||
}
|
||||
if (left != undefined && right != undefined) {
|
||||
if (fullLines) {
|
||||
return line.slice(0, line.length);
|
||||
} else {
|
||||
return line.slice(left, right);
|
||||
.map(loc => {
|
||||
return textLines.map((line, index0) => {
|
||||
let index = index0 + 1;
|
||||
let left, right;
|
||||
if (index == loc.first_line) {
|
||||
left = loc.first_column;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
if (index == loc.last_line) {
|
||||
right = loc.last_column;
|
||||
}
|
||||
if (index > loc.first_line) {
|
||||
left = 0;
|
||||
}
|
||||
if (index < loc.last_line) {
|
||||
right = line.length;
|
||||
}
|
||||
if (left != undefined && right != undefined) {
|
||||
if (fullLines) {
|
||||
return line.slice(0, line.length);
|
||||
} else {
|
||||
return line.slice(left, right);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}).filter(text => text != "").join("\n");
|
||||
}).filter(text => text != "").join("\n");
|
||||
}).filter(text => text != "").join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { SlicedExecution } from "../../slicing/ExecutionSlicer";
|
||||
|
||||
/**
|
||||
* An interface for copyings cells to the clipboard.
|
||||
*/
|
||||
export interface ICellClipboard {
|
||||
/**
|
||||
* Copy cells in a slice to the clipboard
|
||||
*/
|
||||
copy: (slice: SlicedExecution) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to changes to the clipboard.
|
||||
*/
|
||||
export interface IClipboardListener {
|
||||
/**
|
||||
* Called when something is copied to the clipboard.
|
||||
*/
|
||||
onCopy: (slice: SlicedExecution, clipboard: ICellClipboard) => void;
|
||||
}
|
|
@ -2,9 +2,10 @@ import { IGatherObserver, GatherModel, GatherModelEvent, GatherEventData, Gather
|
|||
import { ExecutionLogSlicer } from "../../slicing/ExecutionSlicer";
|
||||
import { DefSelection, OutputSelection } from "./selections";
|
||||
import { LocationSet } from "../../slicing/Slice";
|
||||
import { ICellClipboard } from "./clipboard";
|
||||
import { INotebookOpener, IScriptOpener } from "./opener";
|
||||
import { log } from "../../utils/log";
|
||||
import { NotebookOpener, ScriptOpener, Clipboard } from "../../lab/gather-actions";
|
||||
import { DocumentManager } from "@jupyterlab/docmanager";
|
||||
import { INotebookTracker } from "@jupyterlab/notebook";
|
||||
|
||||
/**
|
||||
* Controller for updating the gather model.
|
||||
|
@ -13,13 +14,12 @@ export class GatherController implements IGatherObserver {
|
|||
/**
|
||||
* Constructor for gather controller.
|
||||
*/
|
||||
constructor(model: GatherModel, executionSlicer: ExecutionLogSlicer, clipboard: ICellClipboard,
|
||||
notebookOpener?: INotebookOpener, scriptOpener?: IScriptOpener) {
|
||||
constructor(model: GatherModel, documentManager: DocumentManager, notebooks: INotebookTracker) {
|
||||
model.addObserver(this);
|
||||
this._executionSlicer = executionSlicer;
|
||||
this._cellClipboard = clipboard;
|
||||
this._notebookOpener = notebookOpener;
|
||||
this._scriptOpener = scriptOpener;
|
||||
this._executionSlicer = model.executionLog;
|
||||
this._cellClipboard = Clipboard.getInstance();
|
||||
this._notebookOpener = new NotebookOpener(documentManager, notebooks);
|
||||
this._scriptOpener = new ScriptOpener(documentManager, notebooks);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,7 +103,7 @@ export class GatherController implements IGatherObserver {
|
|||
}
|
||||
|
||||
private _executionSlicer: ExecutionLogSlicer;
|
||||
private _cellClipboard: ICellClipboard;
|
||||
private _notebookOpener: INotebookOpener;
|
||||
private _scriptOpener: IScriptOpener;
|
||||
private _cellClipboard: Clipboard;
|
||||
private _notebookOpener: NotebookOpener;
|
||||
private _scriptOpener: ScriptOpener;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { DefSelection, SliceSelection, EditorDef, OutputSelection, CellOutput } from "./selections";
|
||||
import { ICell } from "../cell";
|
||||
import { log } from "../../utils/log";
|
||||
import { SlicedExecution } from "../../slicing/ExecutionSlicer";
|
||||
import { SlicedExecution, ExecutionLogSlicer } from "../../slicing/ExecutionSlicer";
|
||||
import { CellProgram } from "../../slicing/ProgramBuilder";
|
||||
|
||||
/**
|
||||
* Available states for the gathering application.
|
||||
|
@ -52,6 +53,11 @@ export type GatherEventData =
|
|||
* Model for the state of a "gather" application.
|
||||
*/
|
||||
export class GatherModel {
|
||||
|
||||
constructor(executionLog: ExecutionLogSlicer) {
|
||||
this._executionLog = executionLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an observer to listen to changes to the model.
|
||||
*/
|
||||
|
@ -68,6 +74,17 @@ export class GatherModel {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get execution history for this notebook.
|
||||
*/
|
||||
get executionLog(): ExecutionLogSlicer {
|
||||
return this._executionLog;
|
||||
}
|
||||
|
||||
getCellProgram(cell: ICell): CellProgram {
|
||||
return this._executionLog.getCellProgram(cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state of the model.
|
||||
*/
|
||||
|
@ -142,10 +159,10 @@ export class GatherModel {
|
|||
/**
|
||||
* Remove the editor def from the list of editor definitions.
|
||||
*/
|
||||
removeEditorDefsForCell(cellId: string) {
|
||||
removeEditorDefsForCell(cellPersistentId: string) {
|
||||
for (let i = this._editorDefs.length - 1; i >= 0; i--) {
|
||||
let editorDef = this._editorDefs[i];
|
||||
if (editorDef.cell.id == cellId) {
|
||||
if (editorDef.cell.persistentId == cellPersistentId) {
|
||||
this._editorDefs.splice(i, 1);
|
||||
this.notifyObservers(GatherModelEvent.EDITOR_DEF_REMOVED, editorDef);
|
||||
}
|
||||
|
@ -332,10 +349,10 @@ export class GatherModel {
|
|||
/**
|
||||
* Deselect all outputs.
|
||||
*/
|
||||
deselectOutputsForCell(cellId: string) {
|
||||
deselectOutputsForCell(cellPersistentId: string) {
|
||||
for (let i = this._selectedOutputs.length - 1; i >= 0; i--) {
|
||||
let output = this._selectedOutputs[i];
|
||||
if (output.cell.id == cellId) {
|
||||
if (output.cell.persistentId == cellPersistentId) {
|
||||
this._selectedOutputs.splice(i, 1);
|
||||
this.notifyObservers(GatherModelEvent.OUTPUT_DESELECTED, output);
|
||||
}
|
||||
|
@ -419,6 +436,7 @@ export class GatherModel {
|
|||
}
|
||||
|
||||
private _state: GatherState = GatherState.SELECTING;
|
||||
private _executionLog: ExecutionLogSlicer;
|
||||
private _observers: IGatherObserver[] = [];
|
||||
private _lastExecutedCell: ICell;
|
||||
private _lastDeletedCell: ICell;
|
||||
|
|
|
@ -13,7 +13,7 @@ import { GatherModel } from '../gather';
|
|||
*/
|
||||
export function buildHistoryModel<TOutputModel>(
|
||||
gatherModel: GatherModel,
|
||||
selectedCellId: string,
|
||||
selectedCellPersistentId: string,
|
||||
executionVersions: SlicedExecution[],
|
||||
includeOutput?: boolean
|
||||
): HistoryModel<TOutputModel> {
|
||||
|
@ -22,9 +22,9 @@ export function buildHistoryModel<TOutputModel>(
|
|||
// recent version, save a mapping from cells' IDs to their content, so we can look them up to
|
||||
// make comparisons between versions of cells.
|
||||
let lastestVersion = executionVersions[executionVersions.length - 1];
|
||||
let latestCellVersions: { [cellId: string]: CellSlice } = {};
|
||||
let latestCellVersions: { [cellPersistentId: string]: CellSlice } = {};
|
||||
lastestVersion.cellSlices.forEach(cellSlice => {
|
||||
latestCellVersions[cellSlice.cell.id] = cellSlice;
|
||||
latestCellVersions[cellSlice.cell.persistentId] = cellSlice;
|
||||
});
|
||||
|
||||
// Compute diffs between each of the previous revisions and the current revision.
|
||||
|
@ -38,7 +38,7 @@ export function buildHistoryModel<TOutputModel>(
|
|||
executionVersion.cellSlices.forEach(function (cellSlice) {
|
||||
|
||||
let cell = cellSlice.cell;
|
||||
let recentCellVersion = latestCellVersions[cell.id];
|
||||
let recentCellVersion = latestCellVersions[cell.persistentId];
|
||||
let latestText: string = "";
|
||||
if (recentCellVersion) {
|
||||
latestText = recentCellVersion.textSlice;
|
||||
|
@ -48,7 +48,7 @@ export function buildHistoryModel<TOutputModel>(
|
|||
let diff = computeTextDiff(latestText, thisVersionText);
|
||||
|
||||
let slicedCell: SlicedCellModel = new SlicedCellModel({
|
||||
cellId: cell.id,
|
||||
cellPersistentId: cell.persistentId,
|
||||
executionCount: cell.executionCount,
|
||||
sourceCode: diff.text,
|
||||
diff: diff
|
||||
|
@ -60,7 +60,7 @@ export function buildHistoryModel<TOutputModel>(
|
|||
if (includeOutput) {
|
||||
let selectedCell: ICell = null;
|
||||
executionVersion.cellSlices.map(cs => cs.cell).forEach(function (cellModel) {
|
||||
if (cellModel.id == selectedCellId) {
|
||||
if (cellModel.persistentId == selectedCellPersistentId) {
|
||||
selectedCell = cellModel;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface ISlicedCellModel extends CodeEditor.IModel {
|
|||
/**
|
||||
* A unique ID for a cell.
|
||||
*/
|
||||
readonly cellId: string;
|
||||
readonly cellPersistentId: string;
|
||||
|
||||
/**
|
||||
* The execution count for the cell.
|
||||
|
@ -38,7 +38,7 @@ export class SlicedCellModel extends CodeEditor.Model implements ISlicedCellMode
|
|||
constructor(options: SlicedCellModel.IOptions) {
|
||||
super({ modelDB: options.modelDB });
|
||||
|
||||
this._cellId = options.cellId;
|
||||
this._cellPersistentId = options.cellPersistentId;
|
||||
this._executionCount = options.executionCount;
|
||||
this._sourceCode = options.sourceCode;
|
||||
this._diff = options.diff;
|
||||
|
@ -50,8 +50,8 @@ export class SlicedCellModel extends CodeEditor.Model implements ISlicedCellMode
|
|||
/**
|
||||
* Get the cell ID.
|
||||
*/
|
||||
get cellId(): string {
|
||||
return this._cellId;
|
||||
get cellPersistentId(): string {
|
||||
return this._cellPersistentId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,7 @@ export class SlicedCellModel extends CodeEditor.Model implements ISlicedCellMode
|
|||
return this._diff;
|
||||
}
|
||||
|
||||
private _cellId: string;
|
||||
private _cellPersistentId: string;
|
||||
private _executionCount: number;
|
||||
private _sourceCode: string;
|
||||
private _diff:Diff;
|
||||
|
@ -93,7 +93,7 @@ export namespace SlicedCellModel {
|
|||
/**
|
||||
* A unique ID for a cell.
|
||||
*/
|
||||
cellId: string;
|
||||
cellPersistentId: string;
|
||||
|
||||
/**
|
||||
* The execution count for the cell.
|
||||
|
|
|
@ -79,7 +79,7 @@ export interface ILocation {
|
|||
|
||||
export interface ILocatable {
|
||||
location: ILocation;
|
||||
cellId?: string;
|
||||
cellPersistentId?: string;
|
||||
executionCount?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,12 @@ export class DataflowAnalyzer {
|
|||
}
|
||||
|
||||
private _statementLocationKey(statement: ast.ISyntaxNode) {
|
||||
if (statement.cellId != undefined && statement.executionCount != undefined) {
|
||||
if (statement.cellPersistentId != undefined && statement.executionCount != undefined) {
|
||||
return statement.location.first_line + "," +
|
||||
statement.location.first_column + "," +
|
||||
statement.location.last_line + "," +
|
||||
statement.location.last_column + "," +
|
||||
statement.cellId + "," +
|
||||
statement.cellPersistentId + "," +
|
||||
statement.executionCount;
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -8,11 +8,17 @@ import { DataflowAnalyzer } from "./DataflowAnalysis";
|
|||
*/
|
||||
export class CellExecution {
|
||||
constructor(
|
||||
public cellId: string,
|
||||
public executionCount: number,
|
||||
public executionTime: Date,
|
||||
public hasError: boolean
|
||||
public readonly cell: ICell,
|
||||
public readonly executionTime: Date
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Update this method if at some point we only want to save some about a CellExecution when
|
||||
* serializing it and saving history.
|
||||
*/
|
||||
toJSON(): any {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,18 +31,18 @@ export class SlicedExecution {
|
|||
) { }
|
||||
|
||||
merge(...slicedExecutions: SlicedExecution[]): SlicedExecution {
|
||||
let cellSlices: { [ cellId: string ]: { [ executionCount: number ]: CellSlice }} = {};
|
||||
let cellSlices: { [ cellPersistentId: string ]: { [ executionCount: number ]: CellSlice }} = {};
|
||||
let mergedCellSlices = [];
|
||||
for (let slicedExecution of slicedExecutions.concat(this)) {
|
||||
for (let cellSlice of slicedExecution.cellSlices) {
|
||||
let cell = cellSlice.cell;
|
||||
if (!cellSlices.hasOwnProperty(cell.id)) cellSlices[cell.id] = {};
|
||||
if (!cellSlices[cell.id].hasOwnProperty(cell.executionCount)) {
|
||||
if (!cellSlices.hasOwnProperty(cell.persistentId)) cellSlices[cell.persistentId] = {};
|
||||
if (!cellSlices[cell.persistentId].hasOwnProperty(cell.executionCount)) {
|
||||
let newCellSlice = new CellSlice(cell.copy(), new LocationSet(), cellSlice.executionTime);
|
||||
cellSlices[cell.id][cell.executionCount] = newCellSlice;
|
||||
cellSlices[cell.persistentId][cell.executionCount] = newCellSlice;
|
||||
mergedCellSlices.push(newCellSlice);
|
||||
}
|
||||
let mergedCellSlice = cellSlices[cell.id][cell.executionCount];
|
||||
let mergedCellSlice = cellSlices[cell.persistentId][cell.executionCount];
|
||||
mergedCellSlice.slice = mergedCellSlice.slice.union(cellSlice.slice);
|
||||
}
|
||||
}
|
||||
|
@ -65,11 +71,20 @@ export class ExecutionLogSlicer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a cell execution to the log.
|
||||
* Log that a cell has just been executed.
|
||||
*/
|
||||
public logExecution(cell: ICell) {
|
||||
this._programBuilder.add(cell);
|
||||
this._executionLog.push(new CellExecution(cell.id, cell.executionCount, new Date(), cell.hasError));
|
||||
let cellExecution = new CellExecution(cell, new Date());
|
||||
this.addExecutionToLog(cellExecution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use logExecution instead if a cell has just been run. This function is intended to be used
|
||||
* only to initialize history when a notebook is reloaded.
|
||||
*/
|
||||
public addExecutionToLog(cellExecution: CellExecution) {
|
||||
this._programBuilder.add(cellExecution.cell);
|
||||
this._executionLog.push(cellExecution);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,19 +111,19 @@ export class ExecutionLogSlicer {
|
|||
public sliceAllExecutions(cell: ICell, pSeedLocations?: LocationSet): SlicedExecution[] {
|
||||
|
||||
// Make a map from cells to their execution times.
|
||||
let cellExecutionTimes: { [cellId: string]: { [executionCount: number]: Date } } = {};
|
||||
let cellExecutionTimes: { [cellPersistentId: string]: { [executionCount: number]: Date } } = {};
|
||||
for (let execution of this._executionLog) {
|
||||
if (!cellExecutionTimes[execution.cellId]) cellExecutionTimes[execution.cellId] = {};
|
||||
cellExecutionTimes[execution.cellId][execution.executionCount] = execution.executionTime;
|
||||
if (!cellExecutionTimes[execution.cell.persistentId]) cellExecutionTimes[execution.cell.persistentId] = {};
|
||||
cellExecutionTimes[execution.cell.persistentId][execution.cell.executionCount] = execution.executionTime;
|
||||
}
|
||||
|
||||
return this._executionLog
|
||||
.filter(execution => execution.cellId == cell.id)
|
||||
.filter(execution => execution.executionCount != undefined)
|
||||
.filter(execution => execution.cell.persistentId == cell.persistentId)
|
||||
.filter(execution => execution.cell.executionCount != undefined)
|
||||
.map(execution => {
|
||||
|
||||
// Build the program up to that cell.
|
||||
let program = this._programBuilder.buildTo(execution.cellId, execution.executionCount);
|
||||
let program = this._programBuilder.buildTo(execution.cell.persistentId, execution.cell.executionCount);
|
||||
if (program == null) return null;
|
||||
|
||||
// Set the seed locations for the slice.
|
||||
|
@ -124,7 +139,7 @@ export class ExecutionLogSlicer {
|
|||
}
|
||||
|
||||
// Set seed locations were specified relative to the last cell's position in program.
|
||||
let lastCellLines = program.cellToLineMap[execution.cellId][execution.executionCount];
|
||||
let lastCellLines = program.cellToLineMap[execution.cell.persistentId][execution.cell.executionCount];
|
||||
let lastCellStart = Math.min(...lastCellLines.items);
|
||||
seedLocations = new LocationSet(
|
||||
...seedLocations.items.map(loc => {
|
||||
|
@ -146,7 +161,7 @@ export class ExecutionLogSlicer {
|
|||
let cellOrder = new Array<ICell>();
|
||||
sliceLocations.forEach(location => {
|
||||
let sliceCell = program.lineToCellMap[location.first_line];
|
||||
let sliceCellLines = program.cellToLineMap[sliceCell.id][sliceCell.executionCount];
|
||||
let sliceCellLines = program.cellToLineMap[sliceCell.persistentId][sliceCell.executionCount];
|
||||
let sliceCellStart = Math.min(...sliceCellLines.items);
|
||||
if (cellOrder.indexOf(sliceCell) == -1) {
|
||||
cellOrder.push(sliceCell);
|
||||
|
@ -157,20 +172,20 @@ export class ExecutionLogSlicer {
|
|||
last_line: location.last_line - sliceCellStart + 1,
|
||||
last_column: location.last_column
|
||||
};
|
||||
if (!cellSliceLocations[sliceCell.id]) cellSliceLocations[sliceCell.id] = {};
|
||||
if (!cellSliceLocations[sliceCell.id][sliceCell.executionCount]) {
|
||||
cellSliceLocations[sliceCell.id][sliceCell.executionCount] = new LocationSet();
|
||||
if (!cellSliceLocations[sliceCell.persistentId]) cellSliceLocations[sliceCell.persistentId] = {};
|
||||
if (!cellSliceLocations[sliceCell.persistentId][sliceCell.executionCount]) {
|
||||
cellSliceLocations[sliceCell.persistentId][sliceCell.executionCount] = new LocationSet();
|
||||
}
|
||||
cellSliceLocations[sliceCell.id][sliceCell.executionCount].add(adjustedLocation);
|
||||
cellSliceLocations[sliceCell.persistentId][sliceCell.executionCount].add(adjustedLocation);
|
||||
});
|
||||
|
||||
let cellSlices = cellOrder.map((sliceCell): CellSlice => {
|
||||
let executionTime = undefined;
|
||||
if (cellExecutionTimes[sliceCell.id] && cellExecutionTimes[sliceCell.id][sliceCell.executionCount]) {
|
||||
executionTime = cellExecutionTimes[sliceCell.id][sliceCell.executionCount];
|
||||
if (cellExecutionTimes[sliceCell.persistentId] && cellExecutionTimes[sliceCell.persistentId][sliceCell.executionCount]) {
|
||||
executionTime = cellExecutionTimes[sliceCell.persistentId][sliceCell.executionCount];
|
||||
}
|
||||
return new CellSlice(sliceCell,
|
||||
cellSliceLocations[sliceCell.id][sliceCell.executionCount],
|
||||
cellSliceLocations[sliceCell.persistentId][sliceCell.executionCount],
|
||||
executionTime);
|
||||
});
|
||||
return new SlicedExecution(execution.executionTime, cellSlices);
|
||||
|
@ -178,6 +193,10 @@ export class ExecutionLogSlicer {
|
|||
.filter((s) => s != null && s != undefined);
|
||||
}
|
||||
|
||||
get cellExecutions(): ReadonlyArray<CellExecution> {
|
||||
return this._executionLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cell program (tree, defs, uses) for a cell.
|
||||
*/
|
||||
|
|
|
@ -85,7 +85,7 @@ export class ProgramBuilder {
|
|||
// Sanity check that this is actually a node.
|
||||
if (node.hasOwnProperty("type")) {
|
||||
node.executionCount = cell.executionCount;
|
||||
node.cellId = cell.id;
|
||||
node.cellPersistentId = cell.persistentId;
|
||||
}
|
||||
}
|
||||
// By querying for defs and uses right when a cell is added to the log, we
|
||||
|
@ -122,10 +122,10 @@ export class ProgramBuilder {
|
|||
* Build a program from the list of cells. Program will include the cells' contents in
|
||||
* execution order. It will omit cells that raised errors (syntax or runtime).
|
||||
*/
|
||||
buildTo(cellId: string, executionCount?: number): Program {
|
||||
buildTo(cellPersistentId: string, executionCount?: number): Program {
|
||||
|
||||
let cellVersions = this._cellPrograms
|
||||
.filter(cp => cp.cell.id == cellId)
|
||||
.filter(cp => cp.cell.persistentId == cellPersistentId)
|
||||
.map(cp => cp.cell);
|
||||
let lastCell: ICell;
|
||||
if (executionCount) {
|
||||
|
@ -166,11 +166,11 @@ export class ProgramBuilder {
|
|||
for (let l = 0; l < cellLength; l++) { cellLines.push(currentLine + l); }
|
||||
cellLines.forEach(l => {
|
||||
lineToCellMap[l] = cell;
|
||||
if (!cellToLineMap[cell.id]) cellToLineMap[cell.id] = {};
|
||||
if (!cellToLineMap[cell.id][cell.executionCount]) {
|
||||
cellToLineMap[cell.id][cell.executionCount] = new NumberSet();
|
||||
if (!cellToLineMap[cell.persistentId]) cellToLineMap[cell.persistentId] = {};
|
||||
if (!cellToLineMap[cell.persistentId][cell.executionCount]) {
|
||||
cellToLineMap[cell.persistentId][cell.executionCount] = new NumberSet();
|
||||
}
|
||||
cellToLineMap[cell.id][cell.executionCount].add(l);
|
||||
cellToLineMap[cell.persistentId][cell.executionCount].add(l);
|
||||
});
|
||||
|
||||
// Accumulate the code text.
|
||||
|
@ -207,12 +207,12 @@ export class ProgramBuilder {
|
|||
let lastCell = this._cellPrograms
|
||||
.filter(cp => cp.cell.executionCount != null)
|
||||
.sort((cp1, cp2) => cp1.cell.executionCount - cp2.cell.executionCount).pop();
|
||||
return this.buildTo(lastCell.cell.id);
|
||||
return this.buildTo(lastCell.cell.persistentId);
|
||||
}
|
||||
|
||||
getCellProgram(cell: ICell): CellProgram {
|
||||
let matchingPrograms = this._cellPrograms.filter(
|
||||
(cp) => cp.cell.id == cell.id && cp.cell.executionCount == cell.executionCount);
|
||||
(cp) => cp.cell.persistentId == cell.persistentId && cp.cell.executionCount == cell.executionCount);
|
||||
if (matchingPrograms.length >= 1) return matchingPrograms.pop();
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ describe('CellSlice', () => {
|
|||
let cellSlice = new CellSlice({
|
||||
is_cell: true,
|
||||
id: "id",
|
||||
persistentId: "persistent-id",
|
||||
text: [
|
||||
"a = 1",
|
||||
"b = 2",
|
||||
|
@ -19,7 +20,8 @@ describe('CellSlice', () => {
|
|||
executionCount: 1,
|
||||
isCode: true,
|
||||
gathered: false,
|
||||
copy: () => null
|
||||
copy: () => null,
|
||||
toJupyterJSON: () => null
|
||||
}, new LocationSet(
|
||||
{ first_line: 1, first_column: 0, last_line: 1, last_column: 5 },
|
||||
{ first_line: 2, first_column: 4, last_line: 3, last_column: 4 }
|
||||
|
@ -35,6 +37,7 @@ describe('CellSlice', () => {
|
|||
let cellSlice = new CellSlice({
|
||||
is_cell: true,
|
||||
id: "id",
|
||||
persistentId: "persistent-id",
|
||||
text: [
|
||||
"a = 1",
|
||||
"b = 2",
|
||||
|
@ -46,7 +49,8 @@ describe('CellSlice', () => {
|
|||
executionCount: 1,
|
||||
isCode: true,
|
||||
gathered: false,
|
||||
copy: () => null
|
||||
copy: () => null,
|
||||
toJupyterJSON: () => null
|
||||
}, new LocationSet(
|
||||
{ first_line: 1, first_column: 0, last_line: 1, last_column: 5 },
|
||||
{ first_line: 2, first_column: 4, last_line: 3, last_column: 4 }
|
||||
|
|
|
@ -9,12 +9,14 @@ describe('SlicedExecution', () => {
|
|||
let newCell = {
|
||||
is_cell: true,
|
||||
id: id,
|
||||
persistentId: "persistent-id",
|
||||
executionCount: executionCount,
|
||||
text: codeLines.join('\n'),
|
||||
hasError: false,
|
||||
isCode: true,
|
||||
gathered: false,
|
||||
copy: () => newCell
|
||||
copy: () => newCell,
|
||||
toJupyterJSON: () => {}
|
||||
};
|
||||
return newCell;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ describe('program builder', () => {
|
|||
|
||||
function createCell(id: string, executionCount: number, ...codeLines: string[]): ICell {
|
||||
let text = codeLines.join("\n");
|
||||
return { is_cell: true, id, executionCount, text: text, hasError: false, isCode: true,
|
||||
gathered: false, copy: () => null };
|
||||
return { is_cell: true, id, executionCount, persistentId: "persistent-id", text: text,
|
||||
hasError: false, isCode: true, gathered: false, copy: () => null, toJupyterJSON: () => {} };
|
||||
}
|
||||
|
||||
let programBuilder: ProgramBuilder;
|
||||
|
@ -144,7 +144,7 @@ describe('program builder', () => {
|
|||
createCell("id1", 2, "print(1)")
|
||||
);
|
||||
let tree = programBuilder.build().tree;
|
||||
expect(tree.code[0].cellId).to.equal("id1");
|
||||
expect(tree.code[0].cellPersistentId).to.equal("id1");
|
||||
expect(tree.code[0].executionCount).to.equal(2);
|
||||
});
|
||||
});
|
|
@ -51,7 +51,7 @@ export function registerPollers(...pollers: IStatePoller[]) {
|
|||
* Call this after a batch of operations instead of each item, as calls can take a while.
|
||||
*/
|
||||
export function log(eventName: string, data?: any) {
|
||||
|
||||
|
||||
data = data || {};
|
||||
|
||||
if (_ajaxCaller == undefined) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче