Browse Source

Merge pull request #1394 from afshin/restore

Refactor state restoration, help plugin.
Steven Silvester 8 years ago
parent
commit
0ffcf46353

+ 8 - 14
src/about/plugin.ts

@@ -17,10 +17,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   AboutModel, AboutWidget
 } from './';
@@ -33,23 +29,21 @@ const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.about',
   activate: activateAbout,
   autoStart: true,
-  requires: [ICommandPalette, IStateDB, ILayoutRestorer]
+  requires: [ICommandPalette, ILayoutRestorer]
 };
 
 
-function activateAbout(app: JupyterLab, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
+function activateAbout(app: JupyterLab, palette: ICommandPalette, layout: ILayoutRestorer): void {
   const namespace = 'about-jupyterlab';
   const model = new AboutModel();
   const command = `${namespace}:show`;
   const category = 'Help';
-  const tracker = new InstanceTracker<AboutWidget>({
-    restore: {
-      state, layout, command, namespace,
-      args: widget => null,
-      name: widget => 'about',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<AboutWidget>({ namespace });
+
+  layout.restore(tracker, {
+    command,
+    args: () => null,
+    name: () => 'about'
   });
 
   let widget: AboutWidget;

+ 6 - 0
src/application/shell.ts

@@ -313,6 +313,12 @@ defineSignal(ApplicationShell.prototype, 'currentChanged');
  */
 export
 namespace ApplicationShell {
+  /**
+   * The areas of the application shell where widgets can reside.
+   */
+  export
+  type Area = 'main' | 'top' | 'left' | 'right';
+
   /**
    * The options for adding a widget to a side area of the shell.
    */

+ 78 - 85
src/common/instancetracker.ts

@@ -1,14 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  utils
-} from '@jupyterlab/services';
-
-import {
-  JSONObject
-} from 'phosphor/lib/algorithm/json';
-
 import {
   IDisposable
 } from 'phosphor/lib/core/disposable';
@@ -110,35 +102,17 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
   /**
    * Create a new instance tracker.
    *
-   * @param options - The instance tracker configuration options.
+   * @param options - The instantiation options for an instance tracker.
    */
-  constructor(options: InstanceTracker.IOptions<T> = {}) {
-    this._restore = options.restore;
-
-    if (!this._restore) {
-      return;
-    }
-
-    let { command, namespace, layout, registry, state, when } = this._restore;
-    let promises = [state.fetchNamespace(namespace)].concat(when);
-
-    // Immediately (synchronously) register the restored promise with the
-    // layout restorer if one is present.
-    if (layout) {
-      layout.await(this._restored.promise);
-    }
-
-    Promise.all(promises).then(([saved]) => {
-      promises = saved.map(args => {
-        // Execute the command and if it fails, delete the state restore data.
-        return registry.execute(command, args.value)
-          .catch(() => { state.remove(args.id); });
-      });
-      return Promise.all(promises);
-    }).then(() => { this._restored.resolve(void 0); });
-
+  constructor(options: InstanceTracker.IOptions) {
+    this.namespace = options.namespace;
   }
 
+  /**
+   * A namespace for all tracked widgets, (e.g., `notebook`).
+   */
+  readonly namespace: string;
+
   /**
    * A signal emitted when a widget is added or removed from the tracker.
    */
@@ -178,7 +152,7 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
    *
    * @param widget - The widget being added.
    */
-  add(widget: T): void {
+  add(widget: T, options: ILayoutRestorer.IAddOptions = { area: 'main' }): void {
     if (this._widgets.has(widget)) {
       console.warn(`${widget.id} already exists in the tracker.`);
       return;
@@ -189,15 +163,21 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
 
     // Handle widget state restoration.
     if (!injected && this._restore) {
-      let { layout, namespace, state } = this._restore;
+      let { layout, state } = this._restore;
       let widgetName = this._restore.name(widget);
 
       if (widgetName) {
-        let name = `${namespace}:${widgetName}`;
+        let name = `${this.namespace}:${widgetName}`;
+        let data = this._restore.args(widget);
+        let metadata = options;
+
         Private.nameProperty.set(widget, name);
-        state.save(name, this._restore.args(widget));
+        Private.metadataProperty.set(widget, metadata);
+
+        state.save(name, { data, metadata });
+
         if (layout) {
-          layout.add(widget, name);
+          layout.add(widget, name, options);
         }
       }
     }
@@ -298,6 +278,40 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
     return this._widgets.has(widget as any);
   }
 
+  /**
+   * Restore the widgets in this tracker's namespace.
+   *
+   * @param options - The configuration options that describe restoration.
+   *
+   * @returns A promise that resolves when restoration has completed.
+   *
+   * #### Notes
+   * This function should almost never be invoked by client code. Its primary
+   * use case is to be invoked by a layout restorer plugin that handles multiple
+   * instance trackers and, when ready, asks them each to restore their
+   * respective widgets.
+   */
+  restore(options: InstanceTracker.IRestoreOptions<T>): Promise<any> {
+    this._restore = options;
+
+    let { command, registry, state, when } = options;
+    let namespace = this.namespace;
+    let promises = [state.fetchNamespace(namespace)];
+
+    if (when) {
+      promises = promises.concat(when);
+    }
+
+    return Promise.all(promises).then(([saved]) => {
+      return Promise.all(saved.map(item => {
+        let args = (item.value as any).data;
+        // Execute the command and if it fails, delete the state restore data.
+        return registry.execute(command, args)
+          .catch(() => { state.remove(`${namespace}:${args.id}`); });
+      }));
+    });
+  }
+
   /**
    * Save the restore data for a given widget.
    *
@@ -309,10 +323,10 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
       return;
     }
 
-    let { namespace, state } = this._restore;
+    let { state } = this._restore;
     let widgetName = this._restore.name(widget);
     let oldName = Private.nameProperty.get(widget);
-    let newName = widgetName ? `${namespace}:${widgetName}` : null;
+    let newName = widgetName ? `${this.namespace}:${widgetName}` : null;
 
     if (oldName && oldName !== newName) {
       state.remove(oldName);
@@ -322,7 +336,10 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
     Private.nameProperty.set(widget, newName);
 
     if (newName) {
-      state.save(newName, this._restore.args(widget));
+      let data = this._restore.args(widget);
+      let metadata = Private.metadataProperty.get(widget);
+
+      state.save(newName, { data, metadata });
     }
   }
 
@@ -368,7 +385,6 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
 
   private _currentWidget: T = null;
   private _restore: InstanceTracker.IRestoreOptions<T> = null;
-  private _restored = new utils.PromiseDelegate<void>();
   private _widgets = new Set<T>();
 }
 
@@ -384,40 +400,25 @@ defineSignal(InstanceTracker.prototype, 'currentChanged');
 export
 namespace InstanceTracker {
   /**
-   * The state restoration configuration options.
+   * The instantiation options for an instance tracker.
    */
   export
-  interface IRestoreOptions<T extends Widget> {
-    /**
-     * The command to execute when restoring instances.
-     */
-    command: string;
-
-    /**
-     * A function that returns the args needed to restore an instance.
-     */
-    args: (widget: T) => JSONObject;
-
+  interface IOptions {
     /**
-     * A function that returns a unique persistent name for this instance.
-     */
-    name: (widget: T) => string;
-
-    /**
-     * The namespace to occupy in the state database for restoration data.
+     * A namespace for all tracked widgets, (e.g., `notebook`).
      */
     namespace: string;
+  }
 
-    /**
+  /**
+   * The state restoration configuration options.
+   */
+  export
+  interface IRestoreOptions<T extends Widget> extends ILayoutRestorer.IRestoreOptions<T> {
+    /*
      * The layout restorer to use to re-arrange restored tabs.
-     *
-     * #### Notes
-     * If a layout restorer instance is not supplied, widget instances will
-     * still be restored, but their layout within JupyterLab will be arbitrary.
-     * This may be acceptable for widgets that have a pre-defined slot whose
-     * layout cannot be modified.
      */
-    layout?: ILayoutRestorer;
+    layout: ILayoutRestorer;
 
     /**
      * The command registry which holds the restore command.
@@ -428,22 +429,6 @@ namespace InstanceTracker {
      * The state database instance.
      */
     state: IStateDB;
-
-    /**
-     * The point after which it is safe to restore state.
-     */
-    when: Promise<any> | Array<Promise<any>>;
-  }
-
-  /**
-   * The instance tracker constructor options.
-   */
-  export
-  interface IOptions<T extends Widget> {
-    /**
-     * The optional state restoration options.
-     */
-    restore?: IRestoreOptions<T>;
   }
 }
 
@@ -461,6 +446,14 @@ namespace Private {
     value: false
   });
 
+  /**
+   * An attached property for a widget's restore metadata in the state database.
+   */
+  export
+  const metadataProperty = new AttachedProperty<Widget, ILayoutRestorer.IAddOptions>({
+    name: 'metadata'
+  });
+
   /**
    * An attached property for a widget's ID in the state database.
    */

+ 55 - 65
src/console/plugin.ts

@@ -61,10 +61,6 @@ import {
   IServiceManager
 } from '../services';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   IConsoleTracker, ConsolePanel, ConsoleContent
 } from './index';
@@ -85,7 +81,6 @@ const trackerPlugin: JupyterLabPlugin<IConsoleTracker> = {
     ICommandPalette,
     IPathTracker,
     ConsoleContent.IRenderer,
-    IStateDB,
     ILayoutRestorer
   ],
   activate: activateConsole,
@@ -138,25 +133,23 @@ interface ICreateConsoleArgs extends JSONObject {
 /**
  * Activate the console extension.
  */
-function activateConsole(app: JupyterLab, services: IServiceManager, rendermime: IRenderMime, mainMenu: IMainMenu, inspector: IInspector, palette: ICommandPalette, pathTracker: IPathTracker, renderer: ConsoleContent.IRenderer, state: IStateDB, layout: ILayoutRestorer): IConsoleTracker {
+function activateConsole(app: JupyterLab, services: IServiceManager, rendermime: IRenderMime, mainMenu: IMainMenu, inspector: IInspector, palette: ICommandPalette, pathTracker: IPathTracker, renderer: ConsoleContent.IRenderer, layout: ILayoutRestorer): IConsoleTracker {
   let manager = services.sessions;
-
   let { commands, keymap } = app;
   let category = 'Console';
   let command: string;
+  let count = 0;
   let menu = new Menu({ commands, keymap });
 
   // Create an instance tracker for all console panels.
-  const tracker = new InstanceTracker<ConsolePanel>({
-    restore: {
-      state, layout,
-      command: 'console:create',
-      args: panel => ({ id: panel.content.session.id }),
-      name: panel => panel.content.session && panel.content.session.id,
-      namespace: 'console',
-      when: [app.started, manager.ready],
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<ConsolePanel>({ namespace: 'console' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: 'console:create',
+    args: panel => ({ id: panel.content.session.id }),
+    name: panel => panel.content.session && panel.content.session.id,
+    when: manager.ready
   });
 
   // Sync tracker and set the source of the code inspector.
@@ -170,6 +163,51 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   // Set the main menu title.
   menu.title.label = category;
 
+  command = 'console:create';
+  commands.addCommand(command, {
+    execute: (args: ICreateConsoleArgs) => {
+      let name = `Console ${++count}`;
+
+      args = args || {};
+
+      // If we get a session, use it.
+      if (args.id) {
+        return manager.ready.then(() => manager.connectTo(args.id))
+          .then(session => {
+            name = session.path.split('/').pop();
+            name = `Console ${name.match(CONSOLE_REGEX)[1]}`;
+            createConsole(session, name);
+            return session.id;
+          });
+      }
+
+      // Find the correct path for the new session.
+      // Use the given path or the cwd.
+      let path = args.path || pathTracker.path;
+      if (ContentsManager.extname(path)) {
+        path = ContentsManager.dirname(path);
+      }
+      path = `${path}/console-${count}-${utils.uuid()}`;
+
+      // Get the kernel model.
+      return manager.ready.then(() => getKernel(args, name)).then(kernel => {
+        if (!kernel || (kernel && !kernel.id && !kernel.name)) {
+          return;
+        }
+        // Start the session.
+        let options: Session.IOptions = {
+          path,
+          kernelName: kernel.name,
+          kernelId: kernel.id
+        };
+        return manager.startNew(options).then(session => {
+          createConsole(session, name);
+          return session.id;
+        });
+      });
+    }
+  });
+
   command = 'console:create-new';
   commands.addCommand(command, {
     label: 'Start New Console',
@@ -247,54 +285,6 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   palette.addItem({ command, category });
   menu.addItem({ command });
 
-  let count = 0;
-
-  command = 'console:create';
-  commands.addCommand(command, {
-    execute: (args: ICreateConsoleArgs) => {
-      args = args || {};
-
-      let name = `Console ${++count}`;
-
-      // If we get a session, use it.
-      if (args.id) {
-        return manager.ready.then(() => {
-          return manager.connectTo(args.id);
-        }).then(session => {
-          name = session.path.split('/').pop();
-          name = `Console ${name.match(CONSOLE_REGEX)[1]}`;
-          createConsole(session, name);
-          return session.id;
-        });
-      }
-
-      // Find the correct path for the new session.
-      // Use the given path or the cwd.
-      let path = args.path || pathTracker.path;
-      if (ContentsManager.extname(path)) {
-        path = ContentsManager.dirname(path);
-      }
-      path = `${path}/console-${count}-${utils.uuid()}`;
-
-      // Get the kernel model.
-      return manager.ready.then(() => getKernel(args, name)).then(kernel => {
-        if (!kernel || (kernel && !kernel.id && !kernel.name)) {
-          return;
-        }
-        // Start the session.
-        let options: Session.IOptions = {
-          path,
-          kernelName: kernel.name,
-          kernelId: kernel.id
-        };
-        return manager.startNew(options).then(session => {
-          createConsole(session, name);
-          return session.id;
-        });
-      });
-    }
-  });
-
   command = 'console:inject';
   commands.addCommand(command, {
     execute: (args: JSONObject) => {

+ 9 - 16
src/csvwidget/plugin.ts

@@ -17,10 +17,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   CSVWidget, CSVWidgetFactory
 } from './widget';
@@ -38,7 +34,7 @@ const FACTORY = 'Table';
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.csv-handler',
-  requires: [IDocumentRegistry, IStateDB, ILayoutRestorer],
+  requires: [IDocumentRegistry, ILayoutRestorer],
   activate: activateCSVWidget,
   autoStart: true
 };
@@ -47,22 +43,19 @@ const plugin: JupyterLabPlugin<void> = {
 /**
  * Activate the table widget extension.
  */
-function activateCSVWidget(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB, layout: ILayoutRestorer): void {
+function activateCSVWidget(app: JupyterLab, registry: IDocumentRegistry, layout: ILayoutRestorer): void {
   const factory = new CSVWidgetFactory({
     name: FACTORY,
     fileExtensions: ['.csv'],
     defaultFor: ['.csv']
   });
-  const tracker = new InstanceTracker<CSVWidget>({
-    restore: {
-      state, layout,
-      command: 'file-operations:open',
-      args: widget => ({ path: widget.context.path, factory: FACTORY }),
-      name: widget => widget.context.path,
-      namespace: 'csvwidget',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<CSVWidget>({ namespace: 'csvwidget' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: 'file-operations:open',
+    args: widget => ({ path: widget.context.path, factory: FACTORY }),
+    name: widget => widget.context.path
   });
 
   registry.addWidgetFactory(factory);

+ 15 - 25
src/editorwidget/plugin.ts

@@ -21,10 +21,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   IEditorTracker, EditorWidget, EditorWidgetFactory
 } from './widget';
@@ -66,9 +62,7 @@ const cmdIds = {
 export
 const plugin: JupyterLabPlugin<IEditorTracker> = {
   id: 'jupyter.services.editor-handler',
-  requires: [
-    IDocumentRegistry, IStateDB, ILayoutRestorer, IEditorServices
-  ],
+  requires: [IDocumentRegistry, ILayoutRestorer, IEditorServices],
   provides: IEditorTracker,
   activate: activateEditorHandler,
   autoStart: true
@@ -78,7 +72,7 @@ const plugin: JupyterLabPlugin<IEditorTracker> = {
 /**
  * Sets up the editor widget
  */
-function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB, layout: ILayoutRestorer, editorServices: IEditorServices): IEditorTracker {
+function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, layout: ILayoutRestorer, editorServices: IEditorServices): IEditorTracker {
   const factory = new EditorWidgetFactory({
     editorServices,
     factoryOptions: {
@@ -87,16 +81,13 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, sta
       defaultFor: ['*']
     }
   });
-  const tracker = new InstanceTracker<EditorWidget>({
-    restore: {
-      state, layout,
-      command: 'file-operations:open',
-      args: widget => ({ path: widget.context.path, factory: FACTORY }),
-      name: widget => widget.context.path,
-      namespace: 'editor',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<EditorWidget>({ namespace: 'editor' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: 'file-operations:open',
+    args: widget => ({ path: widget.context.path, factory: FACTORY }),
+    name: widget => widget.context.path
   });
 
   // Sync tracker with currently focused widget.
@@ -143,12 +134,12 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, sta
 
   commands.addCommand(cmdIds.lineNumbers, {
     execute: () => { toggleLineNums(); },
-    label: 'Toggle Line Numbers',
+    label: 'Toggle Line Numbers'
   });
 
   commands.addCommand(cmdIds.lineWrap, {
     execute: () => { toggleLineWrap(); },
-    label: 'Toggle Line Wrap',
+    label: 'Toggle Line Wrap'
   });
 
   commands.addCommand(cmdIds.createConsole, {
@@ -161,9 +152,8 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, sta
         path: widget.context.path,
         preferredLanguage: widget.context.model.defaultKernelLanguage
       };
-      commands.execute('console:create', options).then(id => {
-        sessionIdProperty.set(widget, id);
-      });
+      return commands.execute('console:create', options)
+        .then(id => { sessionIdProperty.set(widget, id); });
     },
     label: 'Create Console for Editor'
   });
@@ -185,9 +175,9 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, sta
       const start = editorModel.getOffsetAt(selection.start);
       const end = editorModel.getOffsetAt(selection.end);
       const code = editorModel.value.text.substring(start, end);
-      commands.execute('console:inject', { id, code });
+      return commands.execute('console:inject', { id, code });
     },
-    label: 'Run Code',
+    label: 'Run Code'
   });
 
   return tracker;

+ 9 - 15
src/faq/plugin.ts

@@ -21,10 +21,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   FaqModel, FaqWidget
 } from './widget';
@@ -36,7 +32,7 @@ import {
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.faq',
-  requires: [ICommandPalette, ICommandLinker, IStateDB, ILayoutRestorer],
+  requires: [ICommandPalette, ICommandLinker, ILayoutRestorer],
   activate: activateFAQ,
   autoStart: true
 };
@@ -45,19 +41,17 @@ const plugin: JupyterLabPlugin<void> = {
 /**
  * Activate the FAQ plugin.
  */
-function activateFAQ(app: JupyterLab, palette: ICommandPalette, linker: ICommandLinker, state: IStateDB, layout: ILayoutRestorer): void {
+function activateFAQ(app: JupyterLab, palette: ICommandPalette, linker: ICommandLinker, layout: ILayoutRestorer): void {
   const category = 'Help';
   const command = 'faq-jupyterlab:show';
   const model = new FaqModel();
-  const tracker = new InstanceTracker<FaqWidget>({
-    restore: {
-      state, layout, command,
-      args: widget => null,
-      name: widget => 'faq',
-      namespace: 'faq',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<FaqWidget>({ namespace: 'faq' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command,
+    args: () => null,
+    name: () => 'faq'
   });
 
   let widget: FaqWidget;

+ 2 - 2
src/filebrowser/plugin.ts

@@ -288,9 +288,9 @@ function addCommands(app: JupyterLab, fbWidget: FileBrowser, docManager: IDocume
   commands.addCommand(cmdIds.toggleBrowser, {
     execute: () => {
       if (fbWidget.isHidden) {
-        commands.execute(cmdIds.showBrowser, void 0);
+        return commands.execute(cmdIds.showBrowser, void 0);
       } else {
-        commands.execute(cmdIds.hideBrowser, void 0);
+        return commands.execute(cmdIds.hideBrowser, void 0);
       }
     }
   });

+ 66 - 62
src/help/plugin.ts

@@ -9,18 +9,22 @@ import {
   installMessageHook, Message
 } from 'phosphor/lib/core/messaging';
 
-import {
-  WidgetMessage
-} from 'phosphor/lib/ui/widget';
-
 import {
   Menu
 } from 'phosphor/lib/ui/menu';
 
+import {
+  WidgetMessage
+} from 'phosphor/lib/ui/widget';
+
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
 
+import {
+  InstanceTracker
+} from '../common/instancetracker';
+
 import {
   ICommandPalette
 } from '../commandpalette';
@@ -30,12 +34,12 @@ import {
 } from '../iframe';
 
 import {
-  IMainMenu
-} from '../mainmenu';
+  ILayoutRestorer
+} from '../layoutrestorer';
 
 import {
-  IStateDB
-} from '../statedb';
+  IMainMenu
+} from '../mainmenu';
 
 
 /**
@@ -48,62 +52,52 @@ const LAB_IS_SECURE = window.location.protocol === 'https:';
  */
 const HELP_CLASS = 'jp-Help';
 
-
 /**
- * A list of commands to add to the help widget.
+ * A list of help resources.
  */
 
-const COMMANDS = [
+const RESOURCES = [
   {
     text: 'Scipy Lecture Notes',
-    id: 'help-doc:scipy-lecture-notes',
     url: 'http://www.scipy-lectures.org/'
   },
   {
     text: 'Numpy Reference',
-    id: 'help-doc:numpy-reference',
     url: 'https://docs.scipy.org/doc/numpy/reference/'
   },
   {
     text: 'Scipy Reference',
-    id: 'help-doc:scipy-reference',
     url: 'https://docs.scipy.org/doc/scipy/reference/'
   },
   {
     text: 'Notebook Tutorial',
-    id: 'help-doc:notebook-tutorial',
     url: 'https://nbviewer.jupyter.org/github/jupyter/notebook/' +
       'blob/master/docs/source/examples/Notebook/Notebook Basics.ipynb'
   },
   {
     text: 'Python Reference',
-    id: 'help-doc:python-reference',
     url: 'https://docs.python.org/3.5/'
   },
   {
     text: 'IPython Reference',
-    id: 'help-doc:ipython-reference',
     url: 'https://ipython.org/documentation.html?v=20160707164940'
   },
   {
     text: 'Matplotlib Reference',
-    id: 'help-doc:mathplotlib-reference',
     url: 'http://matplotlib.org/contents.html?v=20160707164940'
   },
   {
     text: 'SymPy Reference',
-    id: 'help-doc:sympy-reference',
     url: 'http://docs.sympy.org/latest/index.html?v=20160707164940'
   },
   {
     text: 'Pandas Reference',
-    id: 'help-doc:pandas-reference',
     url: 'http://pandas.pydata.org/pandas-docs/stable/?v=20160707164940'
   },
   {
     text: 'Markdown Reference',
-    id: 'help-doc:markdown-reference',
-    url: 'https://help.github.com/articles/getting-started-with-writing-and-formatting-on-github/'
+    url: 'https://help.github.com/articles/' +
+      'getting-started-with-writing-and-formatting-on-github/'
   }
 ];
 
@@ -114,7 +108,7 @@ const COMMANDS = [
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.help-handler',
-  requires: [IMainMenu, ICommandPalette, IStateDB],
+  requires: [IMainMenu, ICommandPalette, ILayoutRestorer],
   activate: activateHelpHandler,
   autoStart: true
 };
@@ -127,35 +121,46 @@ const plugin: JupyterLabPlugin<void> = {
  *
  * returns A promise that resolves when the extension is activated.
  */
-function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB): void {
+function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, layout: ILayoutRestorer): void {
+  let iframe: IFrame = null;
   const category = 'Help';
   const namespace = 'help-doc';
-  const key = `${namespace}:show`;
-  const iframe = newIFrame(namespace);
+  const command = `${namespace}:open`;
   const menu = createMenu();
+  const tracker = new InstanceTracker<IFrame>({ namespace });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command,
+    args: widget => ({ isHidden: widget.isHidden, url: widget.url }),
+    name: widget => namespace
+  });
 
   /**
    * Create a new IFrame widget.
-   *
-   * #### Notes
-   * Once layout restoration is fully supported, the hidden state of the IFrame
-   * widget will be handled by the layout restorer and not by the message hook
-   * handler in this function.
    */
-  function newIFrame(id: string): IFrame {
+  function newIFrame(url: string): IFrame {
     let iframe = new IFrame();
     iframe.addClass(HELP_CLASS);
-    iframe.title.label = 'Help';
-    iframe.id = id;
+    iframe.title.label = category;
+    iframe.id = `${namespace}`;
+    iframe.url = url;
+    // Add the iframe to the instance tracker.
+    tracker.add(iframe, { area: 'right' });
 
-    // If the help widget is being hidden, remove its state.
+    // If the help widget visibility changes, update the tracker.
     installMessageHook(iframe, (iframe: IFrame, msg: Message) => {
-      if (msg === WidgetMessage.BeforeHide) {
-        state.remove(key);
+      switch (msg) {
+        case WidgetMessage.AfterShow:
+        case WidgetMessage.BeforeHide:
+          // Wait until hide has completed.
+          requestAnimationFrame(() => { tracker.save(iframe); });
+          break;
+        default:
+          break;
       }
       return true;
     });
-
     return iframe;
   }
 
@@ -171,7 +176,7 @@ function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICom
     menu.addItem({ command: 'faq-jupyterlab:show' });
     menu.addItem({ command: 'classic-notebook:open' });
 
-    COMMANDS.forEach(item => menu.addItem({ command: item.id }));
+    RESOURCES.forEach(args => { menu.addItem({ args, command }); });
 
     menu.addItem({ command: 'statedb:clear' });
 
@@ -192,7 +197,6 @@ function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICom
    */
   function showHelp(): void {
     app.shell.activateRight(iframe.id);
-    state.save(key, { url: iframe.url });
   }
 
   /**
@@ -215,19 +219,32 @@ function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICom
     }
   }
 
-  COMMANDS.forEach(command => app.commands.addCommand(command.id, {
-    label: command.text,
-    execute: () => {
+  app.commands.addCommand(command, {
+    label: args => args['text'] as string,
+    execute: args => {
+      const url = args['url'] as string;
+      const isHidden = args['isHidden'] as boolean || false;
+
       // If help resource will generate a mixed content error, load externally.
-      if (LAB_IS_SECURE && utils.urlParse(command.url).protocol !== 'https:') {
-        window.open(command.url);
+      if (LAB_IS_SECURE && utils.urlParse(url).protocol !== 'https:') {
+        window.open(url);
         return;
       }
+
+      if (iframe) {
+        iframe.url = url;
+        tracker.save(iframe);
+      } else {
+        iframe = newIFrame(url);
+      }
       attachHelp();
-      iframe.url = command.url;
-      showHelp();
+      if (isHidden) {
+        hideHelp();
+      } else {
+        showHelp();
+      }
     }
-  }));
+  });
 
   app.commands.addCommand(`${namespace}:activate`, {
     execute: () => { showHelp(); }
@@ -239,7 +256,7 @@ function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICom
     execute: () => { toggleHelp(); }
   });
 
-  COMMANDS.forEach(item => palette.addItem({ command: item.id, category }));
+  RESOURCES.forEach(args => { palette.addItem({ args, command, category }); });
 
   let openClassicNotebookId = 'classic-notebook:open';
   app.commands.addCommand(openClassicNotebookId, {
@@ -248,17 +265,4 @@ function activateHelpHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICom
   });
   palette.addItem({ command: openClassicNotebookId, category });
   mainMenu.addMenu(menu, {});
-
-  state.fetch(key).then(args => {
-    if (!args) {
-      state.remove(key);
-      return;
-    }
-    let url = args['url'] as string;
-    let filtered = COMMANDS.filter(command => command.url === url);
-    if (filtered.length) {
-      let command = filtered[0];
-      app.commands.execute(command.id, void 0);
-    }
-  });
 }

+ 13 - 20
src/imagewidget/plugin.ts

@@ -21,10 +21,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   ImageWidget, ImageWidgetFactory
 } from './widget';
@@ -47,7 +43,7 @@ const FACTORY = 'Image';
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.image-handler',
-  requires: [IDocumentRegistry, ICommandPalette, IStateDB, ILayoutRestorer],
+  requires: [IDocumentRegistry, ICommandPalette, ILayoutRestorer],
   activate: activateImageWidget,
   autoStart: true
 };
@@ -56,28 +52,21 @@ const plugin: JupyterLabPlugin<void> = {
 /**
  * Activate the image widget extension.
  */
-function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
-  let zoomInImage = 'image-widget:zoom-in';
-  let zoomOutImage = 'image-widget:zoom-out';
-  let resetZoomImage = 'image-widget:reset-zoom';
-
+function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palette: ICommandPalette, layout: ILayoutRestorer): void {
+  const namespace = 'image-widget';
   const factory = new ImageWidgetFactory({
     name: FACTORY,
     modelName: 'base64',
     fileExtensions: EXTENSIONS,
     defaultFor: EXTENSIONS
   });
+  const tracker = new InstanceTracker<ImageWidget>({ namespace });
 
-  const tracker = new InstanceTracker<ImageWidget>({
-    restore: {
-      state, layout,
-      command: 'file-operations:open',
-      args: widget => ({ path: widget.context.path, factory: FACTORY }),
-      name: widget => widget.context.path,
-      namespace: 'imagewidget',
-      when: app.started,
-      registry: app.commands
-    }
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: 'file-operations:open',
+    args: widget => ({ path: widget.context.path, factory: FACTORY }),
+    name: widget => widget.context.path
   });
 
   // Sync tracker with currently focused widget.
@@ -93,6 +82,10 @@ function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palet
     tracker.add(widget);
   });
 
+  let zoomInImage = `${namespace}:zoom-in`;
+  let zoomOutImage = `${namespace}:zoom-out`;
+  let resetZoomImage = `${namespace}:reset-zoom`;
+
   app.commands.addCommand(zoomInImage, {
     execute: zoomIn,
     label: 'Zoom In'

+ 9 - 15
src/inspector/plugin.ts

@@ -18,10 +18,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   IInspector, Inspector
 } from './';
@@ -33,7 +29,7 @@ import {
 export
 const plugin: JupyterLabPlugin<IInspector> = {
   id: 'jupyter.services.inspector',
-  requires: [ICommandPalette, IStateDB, ILayoutRestorer],
+  requires: [ICommandPalette, ILayoutRestorer],
   provides: IInspector,
   activate: activateInspector
 };
@@ -95,20 +91,18 @@ class InspectorManager implements IInspector {
 /**
  * Activate the console extension.
  */
-function activateInspector(app: JupyterLab, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): IInspector {
+function activateInspector(app: JupyterLab, palette: ICommandPalette, layout: ILayoutRestorer): IInspector {
   const category = 'Inspector';
   const command = 'inspector:open';
   const label = 'Open Inspector';
   const manager = new InspectorManager();
-  const tracker = new InstanceTracker<Inspector>({
-    restore: {
-      state, layout, command,
-      args: widget => null,
-      name: widget => 'inspector',
-      namespace: 'inspector',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<Inspector>({ namespace: 'inspector' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command,
+    args: () => null,
+    name: () => 'inspector'
   });
 
   function newInspector(): Inspector {

+ 9 - 17
src/landing/plugin.ts

@@ -25,10 +25,6 @@ import {
   IServiceManager
 } from '../services';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   LandingModel, LandingWidget
 } from './widget';
@@ -40,9 +36,7 @@ import {
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.landing',
-  requires: [
-    IPathTracker, ICommandPalette, IServiceManager, IStateDB, ILayoutRestorer
-  ],
+  requires: [IPathTracker, ICommandPalette, IServiceManager, ILayoutRestorer],
   activate: activateLanding,
   autoStart: true
 };
@@ -53,19 +47,17 @@ const plugin: JupyterLabPlugin<void> = {
 const LANDING_CLASS = 'jp-Landing';
 
 
-function activateLanding(app: JupyterLab, pathTracker: IPathTracker, palette: ICommandPalette, services: IServiceManager, state: IStateDB, layout: ILayoutRestorer): void {
+function activateLanding(app: JupyterLab, pathTracker: IPathTracker, palette: ICommandPalette, services: IServiceManager, layout: ILayoutRestorer): void {
   const category = 'Help';
   const command = 'jupyterlab-landing:show';
   const model = new LandingModel(services.terminals.isAvailable());
-  const tracker = new InstanceTracker<LandingWidget>({
-    restore: {
-      state, layout, command,
-      args: widget => null,
-      name: widget => 'landing',
-      namespace: 'landing',
-      when: app.started,
-      registry: app.commands
-    }
+  const tracker = new InstanceTracker<LandingWidget>({ namespace: 'landing' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command,
+    args: () => null,
+    name: () => 'landing'
   });
 
   let widget: LandingWidget;

+ 148 - 57
src/layoutrestorer/layoutrestorer.ts

@@ -7,6 +7,10 @@ import {
   utils
 } from '@jupyterlab/services';
 
+import {
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
+
 import {
   AttachedProperty
 } from 'phosphor/lib/core/properties';
@@ -15,6 +19,10 @@ import {
   defineSignal, ISignal
 } from 'phosphor/lib/core/signaling';
 
+import {
+  CommandRegistry
+} from 'phosphor/lib/ui/commandregistry';
+
 import {
   Widget
 } from 'phosphor/lib/ui/widget';
@@ -23,6 +31,14 @@ import {
   Token
 } from 'phosphor/lib/core/token';
 
+import {
+  ApplicationShell
+} from '../application/shell';
+
+import {
+  InstanceTracker
+} from '../common/instancetracker';
+
 import {
   IStateDB
 } from '../statedb';
@@ -50,16 +66,64 @@ interface ILayoutRestorer {
   /**
    * Add a widget to be tracked by the layout restorer.
    */
-  add(widget: Widget, name: string): void;
+  add(widget: Widget, name: string, options?: ILayoutRestorer.IAddOptions): void;
 
   /**
-   * Wait for the given promise to resolve before restoring layout.
+   * Restore the widgets of a particular instance tracker.
    *
-   * #### Notes
-   * This function should only be called before the `first` promise passed in
-   * at instantiation has resolved. See the notes for `LayoutRestorer.IOptions`.
+   * @param tracker - The instance tracker whose widgets will be restored.
+   *
+   * @param options - The restoration options.
+   */
+  restore(tracker: InstanceTracker<any>, options: ILayoutRestorer.IRestoreOptions<any>): void;
+}
+
+
+/**
+ * A namespace for layout restorers.
+ */
+export
+namespace ILayoutRestorer {
+  /**
+   * Configuration options for adding a widget to a layout restorer.
+   */
+  export
+  interface IAddOptions extends JSONObject {
+    /**
+     * The area in the application shell where a given widget will be restored.
+     */
+    area: ApplicationShell.Area;
+  }
+
+  /**
+   * The state restoration configuration options.
    */
-  await(promise: Promise<any>): void;
+  export
+  interface IRestoreOptions<T extends Widget> {
+    /**
+     * The command to execute when restoring instances.
+     */
+    command: string;
+
+    /**
+     * A function that returns the args needed to restore an instance.
+     */
+    args: (widget: T) => JSONObject;
+
+    /**
+     * A function that returns a unique persistent name for this instance.
+     */
+    name: (widget: T) => string;
+
+    /**
+     * The point after which it is safe to restore state.
+     *
+     * #### Notes
+     * By definition, this promise or promises will happen after the application
+     * has `started`.
+     */
+    when?: Promise<any> | Array<Promise<any>>;
+  }
 }
 
 
@@ -73,8 +137,38 @@ const KEY = 'layout-restorer:data';
  * The default implementation of a layout restorer.
  *
  * #### Notes
- * The layout restorer requires all of the tabs that will be rearranged and
- * focused to already exist, it does not rehydrate them.
+ * The lifecycle for state and layout restoration is subtle. The sequence of
+ * events is as follows:
+ *
+ * 1. The layout restorer plugin is instantiated.
+ *
+ * 2. Other plugins that care about state and layout restoration require
+ *    the layout restorer as a dependency.
+ *
+ * 3. As each load-time plugin initializes (which happens before the lab
+ *    application has `started`), it instructs the layout restorer whether
+ *    the restorer ought to `restore` its state.
+ *
+ * 4. After all the load-time plugins have finished initializing, the lab
+ *    application `started` promise will resolve. This is the `first`
+ *    promise that the layout restorer waits for. By this point, all of the
+ *    plugins that care about layout restoration will have instructed the
+ *    layout restorer to `restore` their state.
+ *
+ * 5. The layout restorer will then instruct each plugin's instance tracker
+ *    to restore its state and reinstantiate whichever widgets it wants.
+ *
+ * 6. As each instance finishes restoring, it resolves the promise that was
+ *    made to the layout restorer (in step 5).
+ *
+ * 7. After all of the promises that the restorer is awaiting have resolved,
+ *    the restorer then reconstructs the saved layout.
+ *
+ * Of particular note are steps 5 and 6: since state restoration of plugins
+ * is accomplished by executing commands, the command that is used to
+ * restore the state of each plugin must return a promise that only resolves
+ * when the widget has been created and added to the plugin's instance
+ * tracker.
  */
 export
 class LayoutRestorer implements ILayoutRestorer {
@@ -82,15 +176,18 @@ class LayoutRestorer implements ILayoutRestorer {
    * Create a layout restorer.
    */
   constructor(options: LayoutRestorer.IOptions) {
+    this._registry = options.registry;
+    this._shell = options.shell;
     this._state = options.state;
-    options.first.then(() => Promise.all(this._promises))
-      .then(() => {
-        // Release the promises held in memory.
-        this._promises = null;
-        // Restore the application state.
-        return this._restore();
-      })
-      .then(() => { this._restored.resolve(void 0); });
+    options.first.then(() => Promise.all(this._promises)).then(() => {
+      // Release the promises held in memory.
+      this._promises = null;
+      // Release the tracker set.
+      this._trackers.clear();
+      this._trackers = null;
+      // Restore the application state.
+      return this._restore();
+    }).then(() => { this._restored.resolve(void 0); });
   }
 
   /**
@@ -108,26 +205,39 @@ class LayoutRestorer implements ILayoutRestorer {
   /**
    * Add a widget to be tracked by the layout restorer.
    */
-  add(widget: Widget, name: string): void {
+  add(widget: Widget, name: string, options: ILayoutRestorer.IAddOptions = { area: 'main' }): void {
     Private.nameProperty.set(widget, name);
     this._widgets.set(name, widget);
     widget.disposed.connect(() => { this._widgets.delete(name); });
   }
 
   /**
-   * Wait for the given promise to resolve before restoring layout.
+   * Restore the widgets of a particular instance tracker.
+   *
+   * @param tracker - The instance tracker whose widgets will be restored.
    *
-   * #### Notes
-   * This function should only be called before the `first` promise passed in
-   * at instantiation has resolved. See the notes for `LayoutRestorer.IOptions`.
+   * @param options - The restoration options.
    */
-  await(promise: Promise<any>): void {
+  restore(tracker: InstanceTracker<Widget>, options: ILayoutRestorer.IRestoreOptions<Widget>): void {
     if (!this._promises) {
-      console.warn('await can only be called before app has started.');
+      console.warn('restore can only be called before `first` has resolved');
       return;
     }
 
-    this._promises.push(promise);
+    let { namespace } = tracker;
+    if (this._trackers.has(namespace)) {
+      console.warn(`a tracker namespaced ${namespace} was already restored`);
+      return;
+    }
+    this._trackers.add(namespace);
+
+    let { args, command, name, when } = options;
+    this._promises.push(tracker.restore({
+      args, command, name, when,
+      layout: this,
+      registry: this._registry,
+      state: this._state
+    }));
   }
 
   /**
@@ -171,7 +281,10 @@ class LayoutRestorer implements ILayoutRestorer {
 
   private _promises: Promise<any>[] = [];
   private _restored = new utils.PromiseDelegate<void>();
+  private _registry: CommandRegistry = null;
+  private _shell: ApplicationShell = null;
   private _state: IStateDB = null;
+  private _trackers = new Set<string>();
   private _widgets = new Map<string, Widget>();
 }
 
@@ -194,42 +307,20 @@ namespace LayoutRestorer {
      * The initial promise that has to be resolved before layout restoration.
      *
      * #### Notes
-     * The lifecycle for state and layout restoration is subtle. This promise
-     * is intended to equal the JupyterLab application `started` notifier.
-     * The sequence of events is as follows:
-     *
-     * 1. The layout restorer plugin is instantiated.
-     *
-     * 2. Other plugins that care about state and layout restoration require
-     *    the layout restorer as a dependency.
-     *
-     * 3. As each load-time plugin initializes (which happens before the lab
-     *    application has `started`), it instructs the layout restorer whether
-     *    the restorer ought to `await` its state restoration.
-     *
-     * 4. After all the load-time plugins have finished initializing, the lab
-     *    application `started` promise will resolve. This is the `first`
-     *    promise that the layout restorer waits for. By this point, all of the
-     *    plugins that care about layout restoration will have instructed the
-     *    layout restorer to `await` their restoration.
-     *
-     * 5. Each plugin will then proceed to restore its state and reinstantiate
-     *    whichever widgets it wants to restore.
-     *
-     * 6. As each plugin finishes restoring, it resolves the promise that it
-     *    instructed the layout restorer to `await` (in step 3).
-     *
-     * 7. After all of the promises that the restorer is awaiting have resolved,
-     *    the restorer then proceeds to reconstruct the saved layout.
-     *
-     * Of particular note are steps 5 and 6: since state restoration of plugins
-     * is accomplished by executing commands, the command that is used to
-     * restore the state of each plugin must return a promise that only resolves
-     * when the widget has been created and added to the plugin's instance
-     * tracker.
+     * This promise should equal the JupyterLab application `started` notifier.
      */
     first: Promise<any>;
 
+    /**
+     * The application command registry.
+     */
+    registry: CommandRegistry;
+
+    /**
+     * The application shell.
+     */
+    shell: ApplicationShell;
+
     /**
      * The state database instance.
      */

+ 6 - 5
src/layoutrestorer/plugin.ts

@@ -24,14 +24,15 @@ const plugin: JupyterLabPlugin<ILayoutRestorer> = {
   id: 'jupyter.services.layout-restorer',
   requires: [IStateDB],
   activate: (app: JupyterLab, state: IStateDB) => {
-    let layout = new LayoutRestorer({ first: app.started, state });
+    const first = app.started;
+    const registry = app.commands;
+    const shell = app.shell;
+    let layout = new LayoutRestorer({ first, registry, shell, state });
     // Activate widgets that have been restored if necessary.
-    layout.activated.connect((sender, id) => {
-      app.shell.activateMain(id);
-    });
+    layout.activated.connect((sender, id) => { shell.activateMain(id); });
     // After restoration is complete, listen to the shell for updates.
     layout.restored.then(() => {
-      app.shell.currentChanged.connect((sender, args) => {
+      shell.currentChanged.connect((sender, args) => {
         layout.save({ currentWidget: args.newValue });
       });
     });

+ 10 - 17
src/markdownwidget/plugin.ts

@@ -21,10 +21,6 @@ import {
   IRenderMime
 } from '../rendermime';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   MarkdownWidget, MarkdownWidgetFactory
 } from './widget';
@@ -52,24 +48,21 @@ const FACTORY = 'Rendered Markdown';
 export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.rendered-markdown',
-  requires: [IDocumentRegistry, IRenderMime, IStateDB, ILayoutRestorer],
-  activate: (app: JupyterLab, registry: IDocumentRegistry, rendermime: IRenderMime, state: IStateDB, layout: ILayoutRestorer) => {
+  requires: [IDocumentRegistry, IRenderMime, ILayoutRestorer],
+  activate: (app: JupyterLab, registry: IDocumentRegistry, rendermime: IRenderMime, layout: ILayoutRestorer) => {
     const factory = new MarkdownWidgetFactory({
       name: FACTORY,
       fileExtensions: ['.md'],
       rendermime
     });
-
-    const tracker = new InstanceTracker<MarkdownWidget>({
-      restore: {
-        state, layout,
-        command: 'file-operations:open',
-        args: widget => ({ path: widget.context.path, factory: FACTORY }),
-        name: widget => widget.context.path,
-        namespace: 'rendered-markdown',
-        when: app.started,
-        registry: app.commands
-      }
+    const namespace = 'rendered-markdown';
+    const tracker = new InstanceTracker<MarkdownWidget>({ namespace });
+
+    // Handle state restoration.
+    layout.restore(tracker, {
+      command: 'file-operations:open',
+      args: widget => ({ path: widget.context.path, factory: FACTORY }),
+      name: widget => widget.context.path
     });
 
     let icon = `${PORTRAIT_ICON_CLASS} ${TEXTEDITOR_ICON_CLASS}`;

+ 9 - 16
src/notebook/plugin.ts

@@ -46,10 +46,6 @@ import {
   IServiceManager
 } from '../services';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   INotebookTracker, NotebookModelFactory, NotebookPanel, NotebookTracker,
   NotebookWidgetFactory, NotebookActions, Notebook
@@ -135,7 +131,6 @@ const trackerPlugin: JupyterLabPlugin<INotebookTracker> = {
     ICommandPalette,
     IInspector,
     NotebookPanel.IRenderer,
-    IStateDB,
     ILayoutRestorer
   ],
   activate: activateNotebookHandler,
@@ -162,7 +157,7 @@ const rendererPlugin: JupyterLabPlugin<NotebookPanel.IRenderer> = {
 /**
  * Activate the notebook handler extension.
  */
-function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, services: IServiceManager, rendermime: IRenderMime, clipboard: IClipboard, mainMenu: IMainMenu, palette: ICommandPalette, inspector: IInspector, renderer: NotebookPanel.IRenderer, state: IStateDB, layout: ILayoutRestorer): INotebookTracker {
+function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, services: IServiceManager, rendermime: IRenderMime, clipboard: IClipboard, mainMenu: IMainMenu, palette: ICommandPalette, inspector: IInspector, renderer: NotebookPanel.IRenderer, layout: ILayoutRestorer): INotebookTracker {
 
   const factory = new NotebookWidgetFactory({
     name: FACTORY,
@@ -176,16 +171,14 @@ function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, s
     renderer
   });
 
-  const tracker = new NotebookTracker({
-    restore: {
-      state, layout,
-      command: 'file-operations:open',
-      args: widget => ({ path: widget.context.path, factory: FACTORY }),
-      name: widget => widget.context.path,
-      namespace: 'notebook',
-      when: [app.started, services.ready],
-      registry: app.commands
-    }
+  const tracker = new NotebookTracker({ namespace: 'notebook' });
+
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: 'file-operations:open',
+    args: panel => ({ path: panel.context.path, factory: FACTORY }),
+    name: panel => panel.context.path,
+    when: services.ready
   });
 
   // Sync tracker and set the source of the code inspector.

+ 19 - 25
src/terminal/plugin.ts

@@ -33,10 +33,6 @@ import {
   IServiceManager
 } from '../services';
 
-import {
-  IStateDB
-} from '../statedb';
-
 import {
   TerminalWidget
 } from './index';
@@ -60,42 +56,41 @@ export
 const plugin: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.terminal',
   requires: [
-    IServiceManager, IMainMenu, ICommandPalette, IStateDB, ILayoutRestorer
+    IServiceManager, IMainMenu, ICommandPalette, ILayoutRestorer
   ],
   activate: activateTerminal,
   autoStart: true
 };
 
 
-function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
+function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu: IMainMenu, palette: ICommandPalette, layout: ILayoutRestorer): void {
   // Bail if there are no terminals available.
   if (!services.terminals.isAvailable()) {
     console.log('Disabling terminals plugin because they are not available on the server');
     return;
   }
+
+  const category = 'Terminal';
+  const namespace = 'terminal';
+  const tracker = new InstanceTracker<TerminalWidget>({ namespace });
+
   let { commands, keymap } = app;
-  let newTerminalId = 'terminal:create-new';
-  let increaseTerminalFontSize = 'terminal:increase-font';
-  let decreaseTerminalFontSize = 'terminal:decrease-font';
-  let toggleTerminalTheme = 'terminal:toggle-theme';
-  let openTerminalId = 'terminal:open';
+  let newTerminalId = `${namespace}:create-new`;
+  let increaseTerminalFontSize = `${namespace}:increase-font`;
+  let decreaseTerminalFontSize = `${namespace}:decrease-font`;
+  let toggleTerminalTheme = `${namespace}:toggle-theme`;
+  let openTerminalId = `${namespace}:open`;
   let options = {
     background: 'black',
     color: 'white',
     fontSize: 13
   };
 
-  // Create an instance tracker for all terminal widgets.
-  const tracker = new InstanceTracker<TerminalWidget>({
-    restore: {
-      state, layout,
-      command: newTerminalId,
-      args: widget => ({ name: widget.session.name }),
-      name: widget => widget.session && widget.session.name,
-      namespace: 'terminal',
-      when: app.started,
-      registry: app.commands
-    }
+  // Handle state restoration.
+  layout.restore(tracker, {
+    command: newTerminalId,
+    args: widget => ({ name: widget.session.name }),
+    name: widget => widget.session && widget.session.name
   });
 
   // Sync tracker with currently focused widget.
@@ -176,13 +171,12 @@ function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu:
         app.shell.activateMain(widget.id);
       } else {
         // Otherwise, create a new terminal with a given name.
-        commands.execute(newTerminalId, { name });
+        return commands.execute(newTerminalId, { name });
       }
     }
   });
 
   // Add command palette items.
-  let category = 'Terminal';
   [
     newTerminalId,
     increaseTerminalFontSize,
@@ -192,7 +186,7 @@ function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu:
 
   // Add menu items.
   let menu = new Menu({ commands, keymap });
-  menu.title.label = 'Terminal';
+  menu.title.label = category;
   menu.addItem({ command: newTerminalId });
   menu.addItem({ command: increaseTerminalFontSize });
   menu.addItem({ command: decreaseTerminalFontSize });

+ 18 - 15
test/src/common/instancetracker.spec.ts

@@ -12,6 +12,9 @@ import {
 } from '../../../lib/common/instancetracker';
 
 
+const NAMESPACE = 'instance-tracker-test';
+
+
 class TestTracker<T extends Widget> extends InstanceTracker<T> {
   methods: string[] = [];
 
@@ -29,7 +32,7 @@ describe('common/instancetracker', () => {
     describe('#constructor()', () => {
 
       it('should create an InstanceTracker', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         expect(tracker).to.be.an(InstanceTracker);
       });
 
@@ -38,7 +41,7 @@ describe('common/instancetracker', () => {
     describe('#currentChanged', () => {
 
       it('should emit when the current widget has been updated', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         let called = false;
         tracker.currentChanged.connect(() => { called = true; });
@@ -53,12 +56,12 @@ describe('common/instancetracker', () => {
     describe('#currentWidget', () => {
 
       it('should default to null', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         expect(tracker.currentWidget).to.be(null);
       });
 
       it('should be updated by sync if the tracker has the widget', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         tracker.add(widget);
         expect(tracker.currentWidget).to.be(null);
@@ -71,7 +74,7 @@ describe('common/instancetracker', () => {
     describe('#isDisposed', () => {
 
       it('should test whether the tracker is disposed', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         expect(tracker.isDisposed).to.be(false);
         tracker.dispose();
         expect(tracker.isDisposed).to.be(true);
@@ -82,7 +85,7 @@ describe('common/instancetracker', () => {
     describe('#add()', () => {
 
       it('should add a widget to the tracker', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         expect(tracker.has(widget)).to.be(false);
         tracker.add(widget);
@@ -90,7 +93,7 @@ describe('common/instancetracker', () => {
       });
 
       it('should remove an added widget if it is disposed', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         tracker.add(widget);
         expect(tracker.has(widget)).to.be(true);
@@ -103,14 +106,14 @@ describe('common/instancetracker', () => {
     describe('#dispose()', () => {
 
       it('should dispose of the resources used by the tracker', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         expect(tracker.isDisposed).to.be(false);
         tracker.dispose();
         expect(tracker.isDisposed).to.be(true);
       });
 
       it('should be safe to call multiple times', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         expect(tracker.isDisposed).to.be(false);
         tracker.dispose();
         tracker.dispose();
@@ -122,7 +125,7 @@ describe('common/instancetracker', () => {
     describe('#find()', () => {
 
       it('should find a tracked item that matches a filter function', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widgetA = new Widget();
         let widgetB = new Widget();
         let widgetC = new Widget();
@@ -136,7 +139,7 @@ describe('common/instancetracker', () => {
       });
 
       it('should return `null` if no item is found', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widgetA = new Widget();
         let widgetB = new Widget();
         let widgetC = new Widget();
@@ -154,7 +157,7 @@ describe('common/instancetracker', () => {
     describe('#forEach()', () => {
 
       it('should iterate through all the tracked items', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widgetA = new Widget();
         let widgetB = new Widget();
         let widgetC = new Widget();
@@ -174,7 +177,7 @@ describe('common/instancetracker', () => {
     describe('#has()', () => {
 
       it('should return `true` if an item exists in the tracker', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         expect(tracker.has(widget)).to.be(false);
         tracker.add(widget);
@@ -186,7 +189,7 @@ describe('common/instancetracker', () => {
     describe('#sync()', () => {
 
       it('should emit a signal when the current widget is updated', () => {
-        let tracker = new InstanceTracker<Widget>();
+        let tracker = new InstanceTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         let called = false;
         tracker.currentChanged.connect(() => { called = true; });
@@ -201,7 +204,7 @@ describe('common/instancetracker', () => {
     describe('#onCurrentChanged()', () => {
 
       it('should be called when the current widget is changed', () => {
-        let tracker = new TestTracker<Widget>();
+        let tracker = new TestTracker<Widget>({ namespace: NAMESPACE });
         let widget = new Widget();
         tracker.add(widget);
         tracker.sync(widget);

+ 9 - 6
test/src/notebook/tracker.spec.ts

@@ -28,6 +28,9 @@ import {
 } from './utils';
 
 
+const NAMESPACE = 'notebook-tracker-test';
+
+
 class TestTracker extends NotebookTracker {
   methods: string[] = [];
 
@@ -53,7 +56,7 @@ describe('notebook/tracker', () => {
     describe('#constructor()', () => {
 
       it('should create a NotebookTracker', () => {
-        let tracker = new NotebookTracker();
+        let tracker = new NotebookTracker({ namespace: NAMESPACE });
         expect(tracker).to.be.a(NotebookTracker);
       });
 
@@ -62,12 +65,12 @@ describe('notebook/tracker', () => {
     describe('#activeCell', () => {
 
       it('should be `null` if there is no tracked notebook panel', () => {
-        let tracker = new NotebookTracker();
+        let tracker = new NotebookTracker({ namespace: NAMESPACE });
         expect(tracker.activeCell).to.be(null);
       });
 
       it('should be `null` if a tracked notebook has no active cell', () => {
-        let tracker = new NotebookTracker();
+        let tracker = new NotebookTracker({ namespace: NAMESPACE });
         let panel = new NotebookPanel({ rendermime, clipboard, renderer});
         tracker.add(panel);
         tracker.sync(panel);
@@ -75,7 +78,7 @@ describe('notebook/tracker', () => {
       });
 
       it('should be the active cell if a tracked notebook has one', () => {
-        let tracker = new NotebookTracker();
+        let tracker = new NotebookTracker({ namespace: NAMESPACE });
         let panel = new NotebookPanel({ rendermime, clipboard, renderer});
         tracker.add(panel);
         tracker.sync(panel);
@@ -90,7 +93,7 @@ describe('notebook/tracker', () => {
     describe('#activeCellChanged', () => {
 
       it('should emit a signal when the active cell changes', () => {
-        let tracker = new NotebookTracker();
+        let tracker = new NotebookTracker({ namespace: NAMESPACE });
         let panel = new NotebookPanel({ rendermime, clipboard, renderer });
         let count = 0;
         tracker.activeCellChanged.connect(() => { count++; });
@@ -110,7 +113,7 @@ describe('notebook/tracker', () => {
     describe('#onCurrentChanged()', () => {
 
       it('should be called when the active cell changes', () => {
-        let tracker = new TestTracker();
+        let tracker = new TestTracker({ namespace: NAMESPACE });
         let panel = new NotebookPanel({ rendermime, clipboard, renderer});
         tracker.add(panel);
         tracker.sync(panel);