Support persistent history for multiple notebooks in Jupyter Lab

This commit is contained in:
Andrew Head 2019-02-11 19:00:02 -08:00
Родитель 189b3c937c
Коммит d47c210736
38 изменённых файлов: 914 добавлений и 2274 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -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",

110
src/history/load.ts Normal file
Просмотреть файл

@ -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);
}

48
src/history/store.ts Normal file
Просмотреть файл

@ -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;
}
}

83
src/lab/element-finder.ts Normal file
Просмотреть файл

@ -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;
}

172
src/lab/gather-actions.ts Normal file
Просмотреть файл

@ -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 } = {};
}

Просмотреть файл

@ -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();
}

20
src/lab/notification.ts Normal file
Просмотреть файл

@ -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);
}
}

56
src/lab/results.ts Normal file
Просмотреть файл

@ -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);
}
}
}
}

8
src/nb/.gitignore поставляемый
Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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");
}

3
src/nb/python/.gitignore поставляемый
Просмотреть файл

@ -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'],
)

210
src/nb/types/index.d.ts поставляемый
Просмотреть файл

@ -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) {