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 4 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
50 changes: 35 additions & 15 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 @@ -334,20 +334,39 @@ 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));
})
);
});
return Promise.all(promises)
Copy link
Member

Choose a reason for hiding this comment

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

The InstanceTracker#restore() method seems like a good candidate to make async.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed.

.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));
})
);
})
.then(val => {
this._restored.resolve(void 0);
return val;
});
}

/**
* A promise resolved when the instance tracker has been restored.
*
* #### Notes
* This promise is not exposed on the IInstanceTracker interface.
* It is intended to allow for the owner/creator of an instance tracker
* to perform additional actions after restoration in specialized use-cases.
*/
get restored(): Promise<void> {
return this._restored.promise;
}

/**
Expand Down Expand Up @@ -448,6 +467,7 @@ export class InstanceTracker<T extends Widget>
}

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

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