Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore cloned output #5981

Merged
merged 7 commits into from Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 43 additions & 16 deletions packages/apputils/src/instancetracker.ts
Expand Up @@ -7,7 +7,7 @@ import { ArrayExt, each, find } from '@phosphor/algorithm';

import { CommandRegistry } from '@phosphor/commands';

import { ReadonlyJSONObject } from '@phosphor/coreutils';
import { PromiseDelegate, ReadonlyJSONObject } from '@phosphor/coreutils';

import { IDisposable } from '@phosphor/disposable';

Expand Down Expand Up @@ -52,6 +52,19 @@ export interface IInstanceTracker<T extends Widget> extends IDisposable {
*/
readonly size: number;

/**
* A promise that is resolved when the instance tracker has been
* restored from a serialized state.
*
* #### Notes
* Most client code will not need to use this, since they can wait
* for the whole application to restore. However, if an extension
* wants to perform actions during the application restoration, but
* after the restoration of another instance tracker, they can use
* this promise.
*/
readonly restored: Promise<void>;

/**
* Find the first widget in the tracker that satisfies a filter function.
*
Expand Down Expand Up @@ -325,7 +338,11 @@ export class InstanceTracker<T extends Widget>
* multiple instance trackers and, when ready, asks them each to restore their
* respective widgets.
*/
restore(options: InstanceTracker.IRestoreOptions<T>): Promise<any> {
async restore(options: InstanceTracker.IRestoreOptions<T>): Promise<any> {
if (this._hasRestored) {
throw new Error('Instance tracker has already restored');
}
this._hasRestored = true;
const { command, registry, state, when } = options;
const namespace = this.namespace;
const promises = when
Expand All @@ -334,20 +351,28 @@ export class InstanceTracker<T extends Widget>

this._restore = options;

return Promise.all(promises).then(([saved]) => {
return Promise.all(
saved.ids.map((id, index) => {
const value = saved.values[index];
const args = value && (value as any).data;
if (args === undefined) {
return state.remove(id);
}

// Execute the command and if it fails, delete the state restore data.
return registry.execute(command, args).catch(() => state.remove(id));
})
);
});
const [saved] = await Promise.all(promises);
const values = await Promise.all(
saved.ids.map((id, index) => {
const value = saved.values[index];
const args = value && (value as any).data;
if (args === undefined) {
return state.remove(id);
}

// Execute the command and if it fails, delete the state restore data.
return registry.execute(command, args).catch(() => state.remove(id));
})
);
this._restored.resolve(undefined);
return values;
}

/**
* A promise resolved when the instance tracker has been restored.
*/
get restored(): Promise<void> {
return this._restored.promise;
}

/**
Expand Down Expand Up @@ -447,7 +472,9 @@ export class InstanceTracker<T extends Widget>
}
}

private _hasRestored = false;
private _restore: InstanceTracker.IRestoreOptions<T> | null = null;
private _restored = new PromiseDelegate<void>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the attention to alphabetization ✏️📘

private _tracker = new FocusTracker<T>();
private _currentChanged = new Signal<this, T | null>(this);
private _widgetAdded = new Signal<this, T>(this);
Expand Down
2 changes: 2 additions & 0 deletions packages/notebook-extension/package.json
Expand Up @@ -36,13 +36,15 @@
"@jupyterlab/cells": "^1.0.0-alpha.3",
"@jupyterlab/codeeditor": "^1.0.0-alpha.3",
"@jupyterlab/coreutils": "^3.0.0-alpha.3",
"@jupyterlab/docmanager": "^1.0.0-alpha.3",
"@jupyterlab/filebrowser": "^1.0.0-alpha.3",
"@jupyterlab/launcher": "^1.0.0-alpha.3",
"@jupyterlab/mainmenu": "^1.0.0-alpha.3",
"@jupyterlab/notebook": "^1.0.0-alpha.4",
"@jupyterlab/rendermime": "^1.0.0-alpha.3",
"@jupyterlab/services": "^4.0.0-alpha.3",
"@jupyterlab/statusbar": "^1.0.0-alpha.3",
"@phosphor/algorithm": "^1.1.2",
"@phosphor/coreutils": "^1.3.0",
"@phosphor/disposable": "^1.1.2",
"@phosphor/messaging": "^1.2.2",
Expand Down
168 changes: 149 additions & 19 deletions packages/notebook-extension/src/index.ts
Expand Up @@ -11,6 +11,7 @@ import {
import {
Dialog,
ICommandPalette,
InstanceTracker,
MainAreaWidget,
showDialog
} from '@jupyterlab/apputils';
Expand All @@ -26,6 +27,10 @@ import {
URLExt
} from '@jupyterlab/coreutils';

import { IDocumentManager } from '@jupyterlab/docmanager';

import { ArrayExt } from '@phosphor/algorithm';

import { UUID } from '@phosphor/coreutils';

import { DisposableSet } from '@phosphor/disposable';
Expand Down Expand Up @@ -68,7 +73,7 @@ import { ReadonlyJSONObject, JSONValue } from '@phosphor/coreutils';

import { Message, MessageLoop } from '@phosphor/messaging';

import { Menu } from '@phosphor/widgets';
import { Panel, Menu } from '@phosphor/widgets';

/**
* The command IDs used by the notebook plugin.
Expand Down Expand Up @@ -251,6 +256,7 @@ const trackerPlugin: JupyterFrontEndPlugin<INotebookTracker> = {
provides: INotebookTracker,
requires: [
NotebookPanel.IContentFactory,
IDocumentManager,
IEditorServices,
IRenderMimeRegistry
],
Expand Down Expand Up @@ -480,6 +486,7 @@ function activateCellTools(
function activateNotebookHandler(
app: JupyterFrontEnd,
contentFactory: NotebookPanel.IContentFactory,
docManager: IDocumentManager,
editorServices: IEditorServices,
rendermime: IRenderMimeRegistry,
palette: ICommandPalette | null,
Expand Down Expand Up @@ -508,6 +515,11 @@ function activateNotebookHandler(
});
const { commands } = app;
const tracker = new NotebookTracker({ namespace: 'notebook' });
const clonedOutputs = new InstanceTracker<
MainAreaWidget<Private.ClonedOutputArea>
>({
namespace: 'cloned-outputs'
});

// Handle state restoration.
if (restorer) {
Expand All @@ -517,13 +529,22 @@ function activateNotebookHandler(
name: panel => panel.context.path,
when: services.ready
});
restorer.restore(clonedOutputs, {
command: CommandIDs.createOutputView,
args: widget => ({
path: widget.content.path,
index: widget.content.index
}),
name: widget => `${widget.content.path}:${widget.content.index}`,
when: tracker.restored // After the notebook widgets (but not contents).
});
}

let registry = app.docRegistry;
registry.addModelFactory(new NotebookModelFactory({}));
registry.addWidgetFactory(factory);

addCommands(app, services, tracker);
addCommands(app, docManager, services, tracker, clonedOutputs);
if (palette) {
populatePalette(palette, services);
}
Expand Down Expand Up @@ -815,8 +836,10 @@ function activateNotebookHandler(
*/
function addCommands(
app: JupyterFrontEnd,
docManager: IDocumentManager,
services: ServiceManager,
tracker: NotebookTracker
tracker: NotebookTracker,
clonedOutputs: InstanceTracker<MainAreaWidget>
): void {
const { commands, shell } = app;

Expand Down Expand Up @@ -1487,30 +1510,53 @@ function addCommands(
});
commands.addCommand(CommandIDs.createOutputView, {
label: 'Create New View for Output',
execute: args => {
// Clone the OutputArea
const current = getCurrent({ ...args, activate: false });
const nb = current.content;
const content = (nb.activeCell as CodeCell).cloneOutputArea();
execute: async args => {
let cell: CodeCell | undefined;
let current: NotebookPanel | undefined;
// If we are given a notebook path and cell index, then
// use that, otherwise use the current active cell.
let path = args.path as string | undefined | null;
let index = args.index as number | undefined | null;
if (path && index !== undefined && index !== null) {
current = docManager.findWidget(path, FACTORY) as NotebookPanel;
if (!current) {
return;
}
} else {
current = getCurrent({ ...args, activate: false });
if (!current) {
return;
}
cell = current.content.activeCell as CodeCell;
index = current.content.activeCellIndex;
}
// Create a MainAreaWidget
const content = new Private.ClonedOutputArea({
notebook: current,
cell,
index
});
const widget = new MainAreaWidget({ content });
widget.id = `LinkedOutputView-${UUID.uuid4()}`;
widget.title.label = 'Output View';
widget.title.icon = NOTEBOOK_ICON_CLASS;
widget.title.caption = current.title.label
? `For Notebook: ${current.title.label}`
: 'For Notebook:';
widget.addClass('jp-LinkedOutputView');
current.context.addSibling(widget, {
ref: current.id,
mode: 'split-bottom'
});

const updateCloned = () => {
clonedOutputs.save(widget);
};
current.context.pathChanged.connect(updateCloned);
current.content.model.cells.changed.connect(updateCloned);

// Add the cloned output to the output instance tracker.
clonedOutputs.add(widget);

// Remove the output view if the parent notebook is closed.
nb.disposed.connect(
widget.dispose,
widget
);
current.content.disposed.connect(() => {
current.context.pathChanged.disconnect(updateCloned);
current.content.model.cells.changed.disconnect(updateCloned);
widget.dispose();
});
},
isEnabled: isEnabledAndSingleSelected
});
Expand Down Expand Up @@ -2093,3 +2139,87 @@ function populateMenus(
getKernel: current => current.session.kernel
} as IHelpMenu.IKernelUser<NotebookPanel>);
}

/**
* A namespace for module private functionality.
*/
namespace Private {
/**
* A widget hosting a cloned output area.
*/
export class ClonedOutputArea extends Panel {
constructor(options: ClonedOutputArea.IOptions) {
super();
this._notebook = options.notebook;
this._index = options.index !== undefined ? options.index : -1;
this._cell = options.cell || null;
this.id = `LinkedOutputView-${UUID.uuid4()}`;
this.title.label = 'Output View';
this.title.icon = NOTEBOOK_ICON_CLASS;
this.title.caption = this._notebook.title.label
? `For Notebook: ${this._notebook.title.label}`
: 'For Notebook:';
this.addClass('jp-LinkedOutputView');

// Wait for the notebook to be loaded before
// cloning the output area.
this._notebook.context.ready.then(() => {
if (!this._cell) {
this._cell = this._notebook.content.widgets[this._index] as CodeCell;
}
if (!this._cell || this._cell.model.type !== 'code') {
this.dispose();
return;
}
const clone = this._cell.cloneOutputArea();
this.addWidget(clone);
});
}

/**
* The index of the cell in the notebook.
*/
get index(): number {
return this._cell
? ArrayExt.findFirstIndex(
this._notebook.content.widgets,
c => c === this._cell
)
: this._index;
}

/**
* The path of the notebook for the cloned output area.
*/
get path(): string {
return this._notebook.context.path;
}

private _notebook: NotebookPanel;
private _index: number;
private _cell: CodeCell | null = null;
}

/**
* ClonedOutputArea statics.
*/
export namespace ClonedOutputArea {
export interface IOptions {
/**
* The notebook associated with the cloned output area.
*/
notebook: NotebookPanel;

/**
* The cell for which to clone the output area.
*/
cell?: CodeCell;

/**
* If the cell is not available, provide the index
* of the cell for when the notebook is loaded.
*/
index?: number;
}
}
}
3 changes: 3 additions & 0 deletions packages/notebook-extension/tsconfig.json
Expand Up @@ -21,6 +21,9 @@
{
"path": "../coreutils"
},
{
"path": "../docmanager"
},
{
"path": "../filebrowser"
},
Expand Down