Browse Source

Merge pull request #121 from blink1073/notebookpanel-tests

NotebookPanel refactor and tests
Jason Grout 8 years ago
parent
commit
26dc8bb20b

+ 3 - 1
examples/notebook/src/index.ts

@@ -144,7 +144,9 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
       nbWidget.context.kernel.interrupt();
     }
   };
-  let restartHandler = () => nbWidget.restart();
+  let restartHandler = () => {
+    NotebookActions.restart(nbWidget.kernel, nbWidget.node);
+  }
   let switchHandler = () => {
     selectKernelForContext(nbWidget.context, nbWidget.node);
   };

+ 5 - 27
src/console/widget.ts

@@ -509,32 +509,11 @@ class ConsoleWidget extends Widget {
    * Handle a completion requested signal from an editor.
    */
   protected onCompletionRequest(editor: CellEditorWidget, change: ICompletionRequest): void {
-    let contents = {
-      // Only send the current line of code for completion.
-      code: change.currentValue.split('\n')[change.line],
-      cursor_pos: change.ch
-    };
-    let pendingComplete = ++this._pendingComplete;
-    let model = this._completion.model;
-    this._session.kernel.complete(contents).then(value => {
-      // If model has been disposed, bail.
-      if (model.isDisposed) {
-        return;
-      }
-      // If a newer completion requesy has created a pending request, bail.
-      if (pendingComplete !== this._pendingComplete) {
-        return;
-      }
-      // Completion request failures or negative results fail silently.
-      if (value.status !== 'ok') {
-        return;
-      }
-      // Update the model.
-      model.options = value.matches;
-      model.cursor = { start: value.cursor_start, end: value.cursor_end };
-    }).then(() => {
-      model.original = change;
-    });
+    let kernel = this._session.kernel;
+    if (!kernel) {
+      return;
+    }
+    this._completion.model.makeKernelRequest(change, kernel);
   }
 
   /**
@@ -576,7 +555,6 @@ class ConsoleWidget extends Widget {
   private _tooltip: ConsoleTooltip = null;
   private _history: IConsoleHistory = null;
   private _session: INotebookSession = null;
-  private _pendingComplete = 0;
   private _pendingInspect = 0;
 }
 

+ 9 - 9
src/docmanager/context.ts

@@ -30,7 +30,7 @@ import {
 /**
  * An implementation of a document context.
  */
-class Context implements IDocumentContext {
+class Context implements IDocumentContext<IDocumentModel> {
   /**
    * Construct a new document context.
    */
@@ -42,21 +42,21 @@ class Context implements IDocumentContext {
   /**
    * A signal emitted when the kernel changes.
    */
-  get kernelChanged(): ISignal<IDocumentContext, IKernel> {
+  get kernelChanged(): ISignal<Context, IKernel> {
     return Private.kernelChangedSignal.bind(this);
   }
 
   /**
    * A signal emitted when the path changes.
    */
-  get pathChanged(): ISignal<IDocumentContext, string> {
+  get pathChanged(): ISignal<Context, string> {
     return Private.pathChangedSignal.bind(this);
   }
 
   /**
    * A signal emitted when the model is saved or reverted.
    */
-  get dirtyCleared(): ISignal<IDocumentContext, void> {
+  get dirtyCleared(): ISignal<Context, void> {
     return Private.dirtyClearedSignal.bind(this);
   }
 
@@ -285,7 +285,7 @@ class ContextManager implements IDisposable {
   /**
    * Get a context by id.
    */
-  getContext(id: string): IDocumentContext {
+  getContext(id: string): IDocumentContext<IDocumentModel> {
     return this._contexts[id].context;
   }
 
@@ -531,7 +531,7 @@ namespace Private {
    */
   export
   interface IContextEx {
-    context: IDocumentContext;
+    context: IDocumentContext<IDocumentModel>;
     model: IDocumentModel;
     session: INotebookSession;
     opts: IContentsOpts;
@@ -544,17 +544,17 @@ namespace Private {
    * A signal emitted when the kernel changes.
    */
   export
-  const kernelChangedSignal = new Signal<IDocumentContext, IKernel>();
+  const kernelChangedSignal = new Signal<Context, IKernel>();
 
   /**
    * A signal emitted when the path changes.
    */
   export
-  const pathChangedSignal = new Signal<IDocumentContext, string>();
+  const pathChangedSignal = new Signal<Context, string>();
 
   /**
    * A signal emitted when the model is saved or reverted.
    */
   export
-  const dirtyClearedSignal = new Signal<IDocumentContext, void>();
+  const dirtyClearedSignal = new Signal<Context, void>();
 }

+ 9 - 8
src/docmanager/manager.ts

@@ -36,7 +36,8 @@ import {
 } from '../filebrowser/browser';
 
 import {
-  DocumentRegistry, IDocumentContext, IWidgetFactory, IWidgetFactoryOptions
+  DocumentRegistry, IDocumentContext, IWidgetFactory, IWidgetFactoryOptions,
+  IDocumentModel
 } from '../docregistry';
 
 import {
@@ -312,12 +313,12 @@ class DocumentManager implements IDisposable {
     let model = this._contextManager.getModel(id);
     model.initialize();
     let context = this._contextManager.getContext(id);
-    let child = factory.createNew(model, context, kernel);
+    let child = factory.createNew(context, kernel);
     parent.setContent(child);
     // Handle widget extensions.
     let disposables = new DisposableSet();
     for (let extender of this._registry.getWidgetExtensions(parent.name)) {
-      disposables.add(extender.createNew(child, model, context));
+      disposables.add(extender.createNew(child, context));
     }
     parent.disposed.connect(() => {
       disposables.dispose();
@@ -348,7 +349,7 @@ class DocumentWrapper extends Widget {
   /**
    * Construct a new document widget.
    */
-  constructor(name: string, id: string, manager: ContextManager, factory: IWidgetFactory<Widget>, widgets: { [key: string]: DocumentWrapper[] }) {
+  constructor(name: string, id: string, manager: ContextManager, factory: IWidgetFactory<Widget, IDocumentModel>, widgets: { [key: string]: DocumentWrapper[] }) {
     super();
     this.addClass(DOCUMENT_CLASS);
     this.layout = new PanelLayout();
@@ -376,7 +377,7 @@ class DocumentWrapper extends Widget {
    * #### Notes
    * This is a read-only property.
    */
-  get context(): IDocumentContext {
+  get context(): IDocumentContext<IDocumentModel> {
     return this._manager.getContext(this._id);
   }
 
@@ -445,7 +446,7 @@ class DocumentWrapper extends Widget {
     this._maybeClose(model.dirty).then(result => {
       if (result) {
         // Let the widget factory handle closing.
-        return this._factory.beforeClose(model, this.context, child);
+        return this._factory.beforeClose(child, this.context);
       }
       return result;
     }).then(result => {
@@ -517,7 +518,7 @@ class DocumentWrapper extends Widget {
   }
 
   private _manager: ContextManager = null;
-  private _factory: IWidgetFactory<Widget> = null;
+  private _factory: IWidgetFactory<Widget, IDocumentModel> = null;
   private _id = '';
   private _name = '';
   private _widgets: { [key: string]: DocumentWrapper[] } = null;
@@ -539,6 +540,6 @@ namespace Private {
    */
   export
   interface IWidgetFactoryEx extends IWidgetFactoryOptions {
-    factory: IWidgetFactory<Widget>;
+    factory: IWidgetFactory<Widget, IDocumentModel>;
   }
 }

+ 3 - 3
src/docregistry/default.ts

@@ -264,7 +264,7 @@ class Base64ModelFactory extends TextModelFactory {
  * The default implemetation of a widget factory.
  */
 export
-abstract class ABCWidgetFactory implements IWidgetFactory<Widget> {
+abstract class ABCWidgetFactory implements IWidgetFactory<Widget, IDocumentModel> {
   /**
    * Get whether the model factory has been disposed.
    */
@@ -282,7 +282,7 @@ abstract class ABCWidgetFactory implements IWidgetFactory<Widget> {
   /**
    * Create a new widget given a document model and a context.
    */
-  abstract createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): Widget;
+  abstract createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernelId): Widget;
 
   /**
    * Take an action on a widget before closing it.
@@ -290,7 +290,7 @@ abstract class ABCWidgetFactory implements IWidgetFactory<Widget> {
    * @returns A promise that resolves to true if the document should close
    *   and false otherwise.
    */
-  beforeClose(model: IDocumentModel, context: IDocumentContext, widget: Widget): Promise<boolean> {
+  beforeClose(widget: Widget, context: IDocumentContext<IDocumentModel>): Promise<boolean> {
     // There is nothing specific to do.
     return Promise.resolve(true);
   }

+ 9 - 9
src/docregistry/interfaces.ts

@@ -104,7 +104,7 @@ interface IDocumentModel extends IDisposable {
 /**
  * The document context object.
  */
-export interface IDocumentContext extends IDisposable {
+export interface IDocumentContext<T extends IDocumentModel> extends IDisposable {
   /**
    * The unique id of the context.
    *
@@ -119,7 +119,7 @@ export interface IDocumentContext extends IDisposable {
    * #### Notes
    * This is a read-only property
    */
-  model: IDocumentModel;
+  model: T;
 
   /**
    * The current kernel associated with the document.
@@ -157,12 +157,12 @@ export interface IDocumentContext extends IDisposable {
   /**
    * A signal emitted when the kernel changes.
    */
-  kernelChanged: ISignal<IDocumentContext, IKernel>;
+  kernelChanged: ISignal<IDocumentContext<T>, IKernel>;
 
   /**
    * A signal emitted when the path changes.
    */
-  pathChanged: ISignal<IDocumentContext, string>;
+  pathChanged: ISignal<IDocumentContext<T>, string>;
 
   /**
    * Change the current kernel associated with the document.
@@ -254,11 +254,11 @@ interface IWidgetFactoryOptions {
  * The interface for a widget factory.
  */
 export
-interface IWidgetFactory<T extends Widget> extends IDisposable {
+interface IWidgetFactory<T extends Widget, U extends IDocumentModel> extends IDisposable {
   /**
    * Create a new widget.
    */
-  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): T;
+  createNew(context: IDocumentContext<U>, kernel?: IKernelId): T;
 
   /**
    * Take an action on a widget before closing it.
@@ -266,7 +266,7 @@ interface IWidgetFactory<T extends Widget> extends IDisposable {
    * @returns A promise that resolves to true if the document should close
    *   and false otherwise.
    */
-  beforeClose(model: IDocumentModel, context: IDocumentContext, widget: Widget): Promise<boolean>;
+  beforeClose(widget: T, context: IDocumentContext<U>): Promise<boolean>;
 }
 
 
@@ -274,11 +274,11 @@ interface IWidgetFactory<T extends Widget> extends IDisposable {
  * An interface for a widget extension.
  */
 export
-interface IWidgetExtension<T extends Widget> {
+interface IWidgetExtension<T extends Widget, U extends IDocumentModel> {
   /**
    * Create a new extension for a given widget.
    */
-   createNew(widget: T, model: IDocumentModel, context: IDocumentContext): IDisposable;
+   createNew(widget: T, context: IDocumentContext<U>): IDisposable;
 }
 
 

+ 2 - 2
src/docregistry/kernelselector.ts

@@ -10,7 +10,7 @@ import {
 } from '../dialog';
 
 import {
-  IDocumentContext
+  IDocumentContext, IDocumentModel
 } from './interfaces';
 
 
@@ -95,7 +95,7 @@ function selectKernel(options: IKernelSelection): Promise<IKernelId> {
  * Change the kernel on a context.
  */
 export
-function selectKernelForContext(context: IDocumentContext, host?: HTMLElement): Promise<void> {
+function selectKernelForContext(context: IDocumentContext<IDocumentModel>, host?: HTMLElement): Promise<void> {
   return context.listSessions().then(sessions => {
     let options = {
       name: context.path.split('/').pop(),

+ 8 - 7
src/docregistry/registry.ts

@@ -14,7 +14,8 @@ import {
 
 import {
   IModelFactory, IWidgetFactory, IWidgetFactoryOptions,
-  IFileType, IKernelPreference, IFileCreator, IWidgetExtension
+  IFileType, IKernelPreference, IFileCreator, IWidgetExtension,
+  IDocumentModel
 } from './interfaces';
 
 
@@ -74,7 +75,7 @@ class DocumentRegistry implements IDisposable {
    * If a factory is already registered as a default for a given extension or
    * as the global default, this factory will override the existing default.
    */
-  addWidgetFactory(factory: IWidgetFactory<Widget>, options: IWidgetFactoryOptions): IDisposable {
+  addWidgetFactory(factory: IWidgetFactory<Widget, IDocumentModel>, options: IWidgetFactoryOptions): IDisposable {
     let name = options.displayName;
     let exOpt = utils.copy(options) as Private.IWidgetFactoryEx;
     exOpt.factory = factory;
@@ -136,7 +137,7 @@ class DocumentRegistry implements IDisposable {
    *
    * @returns A disposable which will unregister the extension.
    */
-  addWidgetExtension(widgetName: string, extension: IWidgetExtension<Widget>): IDisposable {
+  addWidgetExtension(widgetName: string, extension: IWidgetExtension<Widget, IDocumentModel>): IDisposable {
     if (!(widgetName in this._extenders)) {
       this._extenders[widgetName] = [];
     }
@@ -319,7 +320,7 @@ class DocumentRegistry implements IDisposable {
    *
    * @returns A widget factory instance.
    */
-  getWidgetFactory(widgetName: string): IWidgetFactory<Widget> {
+  getWidgetFactory(widgetName: string): IWidgetFactory<Widget, IDocumentModel> {
     return this._getWidgetFactoryEx(widgetName).factory;
   }
 
@@ -330,7 +331,7 @@ class DocumentRegistry implements IDisposable {
    *
    * @returns A new array of widget extensions.
    */
-  getWidgetExtensions(widgetName: string): IWidgetExtension<Widget>[] {
+  getWidgetExtensions(widgetName: string): IWidgetExtension<Widget, IDocumentModel>[] {
     if (!(widgetName in this._extenders)) {
       return [];
     }
@@ -356,7 +357,7 @@ class DocumentRegistry implements IDisposable {
   private _defaultWidgetFactories: { [key: string]: string } = Object.create(null);
   private _fileTypes: IFileType[] = [];
   private _creators: IFileCreator[] = [];
-  private _extenders: { [key: string] : IWidgetExtension<Widget>[] } = Object.create(null);
+  private _extenders: { [key: string] : IWidgetExtension<Widget, IDocumentModel>[] } = Object.create(null);
 }
 
 
@@ -369,6 +370,6 @@ namespace Private {
    */
   export
   interface IWidgetFactoryEx extends IWidgetFactoryOptions {
-    factory: IWidgetFactory<Widget>;
+    factory: IWidgetFactory<Widget, IDocumentModel>;
   }
 }

+ 6 - 5
src/editorwidget/widget.ts

@@ -42,10 +42,11 @@ class EditorWidget extends CodeMirrorWidget {
   /**
    * Construct a new editor widget.
    */
-  constructor(model: IDocumentModel, context: IDocumentContext) {
+  constructor(context: IDocumentContext<IDocumentModel>) {
     super();
     this.addClass(EDITOR_CLASS);
     let editor = this.editor;
+    let model = context.model;
     editor.setOption('lineNumbers', true);
     let doc = editor.getDoc();
     doc.setValue(model.toString());
@@ -84,14 +85,14 @@ class EditorWidget extends CodeMirrorWidget {
  * A widget factory for editors.
  */
 export
-class EditorWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<EditorWidget> {
+class EditorWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<EditorWidget, IDocumentModel> {
   /**
-   * Create a new widget given a document model and a context.
+   * Create a new widget given a context.
    */
-  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): EditorWidget {
+  createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernelId): EditorWidget {
     if (kernel) {
       context.changeKernel(kernel);
     }
-    return new EditorWidget(model, context);
+    return new EditorWidget(context);
   }
 }

+ 11 - 14
src/imagewidget/widget.ts

@@ -33,20 +33,19 @@ class ImageWidget extends Widget {
   /**
    * Construct a new image widget.
    */
-  constructor(model: IDocumentModel, context: IDocumentContext) {
+  constructor(context: IDocumentContext<IDocumentModel>) {
     super();
-    this._model = model;
     this._context = context;
     this.node.tabIndex = -1;
     this.node.style.overflowX = 'auto';
     this.node.style.overflowY = 'auto';
-    if (model.toString()) {
+    if (context.model.toString()) {
       this.update();
     }
     context.pathChanged.connect(() => {
       this.update();
     });
-    model.contentChanged.connect(() => {
+    context.model.contentChanged.connect(() => {
       this.update();
     });
   }
@@ -58,7 +57,6 @@ class ImageWidget extends Widget {
     if (this.isDisposed) {
       return;
     }
-    this._model = null;
     this._context = null;
     super.dispose();
   }
@@ -69,13 +67,12 @@ class ImageWidget extends Widget {
   protected onUpdateRequest(msg: Message): void {
     this.title.text = this._context.path.split('/').pop();
     let node = this.node as HTMLImageElement;
-    let content = this._model.toString();
-    let model = this._context.contentsModel;
-    node.src = `data:${model.mimetype};${model.format},${content}`;
+    let content = this._context.model.toString();
+    let cm = this._context.contentsModel;
+    node.src = `data:${cm.mimetype};${cm.format},${content}`;
   }
 
-  private _model: IDocumentModel;
-  private _context: IDocumentContext;
+  private _context: IDocumentContext<IDocumentModel>;
 }
 
 
@@ -83,11 +80,11 @@ class ImageWidget extends Widget {
  * A widget factory for images.
  */
 export
-class ImageWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<ImageWidget> {
+class ImageWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<ImageWidget, IDocumentModel> {
   /**
-   * Create a new widget given a document model and a context.
+   * Create a new widget given a context.
    */
-  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): ImageWidget {
-    return new ImageWidget(model, context);
+  createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernelId): ImageWidget {
+    return new ImageWidget(context);
   }
 }

+ 125 - 0
src/notebook/completion/handler.ts

@@ -0,0 +1,125 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IKernel
+} from 'jupyter-js-services';
+
+import {
+  IDisposable
+} from 'phosphor-disposable';
+
+import {
+  BaseCellWidget
+} from '../cells/widget';
+
+import {
+  CellEditorWidget, ITextChange, ICompletionRequest
+} from '../cells/editor';
+
+import {
+  CompletionWidget
+} from './widget';
+
+
+/**
+ * A completion handler for cell widgets.
+ */
+export
+class CellCompletionHandler implements IDisposable {
+  /**
+   * Construct a new completion handler for a widget.
+   */
+  constructor(completion: CompletionWidget) {
+    this._completion = completion;
+    this._completion.selected.connect(this._onCompletionSelected, this);
+  }
+
+  /**
+   * Get whether the completion handler is disposed.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get isDisposed(): boolean {
+    return this._completion === null;
+  }
+
+  /**
+   * Dispose of the resources used by the handler.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._completion = null;
+    this._kernel = null;
+    this._activeCell = null;
+  }
+
+  /**
+   * The kernel used by the completion handler.
+   */
+  get kernel(): IKernel {
+    return this._kernel;
+  }
+  set kernel(value: IKernel) {
+    this._kernel = value;
+  }
+
+  /**
+   * The cell widget used by the completion handler.
+   */
+  get activeCell(): BaseCellWidget {
+    return this._activeCell;
+  }
+  set activeCell(newValue: BaseCellWidget) {
+    let editor: CellEditorWidget;
+    if (this._activeCell && !this._activeCell.isDisposed) {
+      editor = this._activeCell.editor;
+      editor.textChanged.disconnect(this._onTextChanged, this);
+      editor.completionRequested.disconnect(this._onCompletionRequested, this);
+    }
+    this._activeCell = newValue;
+    if (newValue) {
+      editor = newValue.editor;
+      editor.textChanged.connect(this._onTextChanged, this);
+      editor.completionRequested.connect(this._onCompletionRequested, this);
+    }
+  }
+
+  /**
+   * Handle a text changed signal from an editor.
+   */
+  private _onTextChanged(editor: CellEditorWidget, change: ITextChange): void {
+    this._completion.model.handleTextChange(change);
+  }
+
+  /**
+   * Handle a completion requested signal from an editor.
+   */
+  private _onCompletionRequested(editor: CellEditorWidget, change: ICompletionRequest): void {
+    if (!this.kernel) {
+      return;
+    }
+    this._completion.model.makeKernelRequest(change, this.kernel);
+  }
+
+  /**
+   * Handle a completion selected signal from the completion widget.
+   */
+  private _onCompletionSelected(widget: CompletionWidget, value: string): void {
+    if (!this._activeCell) {
+      return;
+    }
+    let patch = this._completion.model.createPatch(value);
+    let editor = this._activeCell.editor.editor;
+    let doc = editor.getDoc();
+    doc.setValue(patch.text);
+    doc.setCursor(doc.posFromIndex(patch.position));
+  }
+
+  private _kernel: IKernel = null;
+  private _activeCell: BaseCellWidget = null;
+  private _completion: CompletionWidget = null;
+}

+ 1 - 0
src/notebook/completion/index.ts

@@ -1,5 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+export * from './handler';
 export * from './model';
 export * from './widget';

+ 66 - 0
src/notebook/completion/model.ts

@@ -1,6 +1,10 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import {
+  IKernel
+} from 'jupyter-js-services';
+
 import {
   IDisposable
 } from 'phosphor-disposable';
@@ -123,6 +127,16 @@ interface ICompletionModel extends IDisposable {
    */
   original: ICompletionRequest;
 
+  /**
+   * Handle a completion request using a kernel.
+   */
+  makeKernelRequest(request: ICompletionRequest, kernel: IKernel): void;
+
+  /**
+   * Handle a text change.
+   */
+  handleTextChange(change: ITextChange): void;
+
   /**
    * Create a resolved patch between the original state and a patch string.
    */
@@ -233,6 +247,57 @@ class CompletionModel implements ICompletionModel {
     this.stateChanged.emit(void 0);
   }
 
+  /**
+   * Make a request using a kernel.
+   */
+  makeKernelRequest(request: ICompletionRequest, kernel: IKernel): void {
+    let contents = {
+      // Only send the current line of code for completion.
+      code: request.currentValue.split('\n')[request.line],
+      cursor_pos: request.ch
+    };
+    let pendingComplete = ++this._pendingComplete;
+    kernel.complete(contents).then(value => {
+      // If we have been disposed, bail.
+      if (this.isDisposed) {
+        return;
+      }
+      // If a newer completion request has created a pending request, bail.
+      if (pendingComplete !== this._pendingComplete) {
+        return;
+      }
+      // Completion request failures or negative results fail silently.
+      if (value.status !== 'ok') {
+        return;
+      }
+      // Update the state.
+      this.options = value.matches;
+      this.cursor = { start: value.cursor_start, end: value.cursor_end };
+    }).then(() => {
+      this.original = request;
+    });
+  }
+
+  /**
+   * Handle a text change.
+   */
+  handleTextChange(change: ITextChange): void {
+    let line = change.newValue.split('\n')[change.line];
+    // If last character entered is not whitespace, update completion.
+    if (line[change.ch - 1] && line[change.ch - 1].match(/\S/)) {
+      // If there is currently a completion
+      if (this.original) {
+        this.current = change;
+      }
+    } else {
+      // If final character is whitespace, reset completion.
+      this.options = null;
+      this.original = null;
+      this.cursor = null;
+      return;
+    }
+  }
+
   /**
    * Create a resolved patch between the original state and a patch string.
    *
@@ -325,6 +390,7 @@ class CompletionModel implements ICompletionModel {
   private _current: ITextChange = null;
   private _query = '';
   private _cursor: { start: number, end: number } = null;
+  private _pendingComplete = 0;
 }
 
 

+ 0 - 1
src/notebook/completion/widget.ts

@@ -122,7 +122,6 @@ class CompletionWidget extends Widget {
     }
     this._model.dispose();
     this._model = null;
-    clearSignalData(this);
     super.dispose();
   }
 

+ 25 - 0
src/notebook/notebook/actions.ts

@@ -9,6 +9,10 @@ import {
   MimeData as IClipboard
 } from 'phosphor-dragdrop';
 
+import {
+  showDialog
+} from '../../dialog';
+
 import {
   ICellModel, CodeCellModel,
   CodeCellWidget, BaseCellWidget, MarkdownCellWidget
@@ -582,6 +586,27 @@ namespace NotebookActions {
       }
     }
   }
+
+  /**
+   * Restart a kernel.
+   */
+  export
+  function restart(kernel: IKernel, host?: HTMLElement): Promise<boolean> {
+    if (!kernel) {
+      return Promise.resolve(false);
+    }
+    return showDialog({
+      title: 'Restart Kernel?',
+      body: 'Do you want to restart the current kernel? All variables will be lost.',
+      host
+    }).then(result => {
+      if (result.text === 'OK') {
+        return kernel.restart().then(() => { return true; });
+      } else {
+        return false;
+      }
+    });
+  }
 }
 
 

+ 27 - 46
src/notebook/notebook/default-toolbar.ts

@@ -5,10 +5,6 @@ import {
   IKernel, KernelStatus
 } from 'jupyter-js-services';
 
-import {
-  IDocumentContext
-} from '../../docregistry';
-
 import {
   Widget
 } from 'phosphor-widget';
@@ -17,6 +13,10 @@ import {
   NotebookPanel
 } from './panel';
 
+import {
+  Notebook
+} from './widget';
+
 import {
   ToolbarButton
 } from './toolbar';
@@ -171,7 +171,7 @@ namespace ToolbarItems {
     return new ToolbarButton({
       className: TOOLBAR_RUN,
       onClick: () => {
-        NotebookActions.runAndAdvance(panel.content, panel.context.kernel);
+        NotebookActions.runAndAdvance(panel.content, panel.kernel);
       },
       tooltip: 'Run the selected cell(s) and advance'
     });
@@ -185,7 +185,7 @@ namespace ToolbarItems {
     return new ToolbarButton({
       className: TOOLBAR_INTERRUPT,
       onClick: () => {
-        if (panel.context.kernel) {
+        if (panel.kernel) {
           panel.context.kernel.interrupt();
         }
       },
@@ -201,7 +201,7 @@ namespace ToolbarItems {
     return new ToolbarButton({
       className: TOOLBAR_RESTART,
       onClick: () => {
-        panel.restart();
+        NotebookActions.restart(panel.kernel, panel.node);
       },
       tooltip: 'Restart the kernel'
     });
@@ -212,7 +212,7 @@ namespace ToolbarItems {
    */
   export
   function createCellTypeItem(panel: NotebookPanel): Widget {
-    return new CellTypeSwitcher(panel);
+    return new CellTypeSwitcher(panel.content);
   }
 
   /**
@@ -223,13 +223,13 @@ namespace ToolbarItems {
     let widget = new Widget();
     widget.addClass(TOOLBAR_KERNEL);
     widget.node.textContent = 'No Kernel!';
-    if (panel.context.kernel) {
-      panel.context.kernel.getKernelSpec().then(spec => {
+    if (panel.kernel) {
+      panel.kernel.getKernelSpec().then(spec => {
         widget.node.textContent = spec.display_name;
       });
     }
-    panel.context.kernelChanged.connect(() => {
-      panel.context.kernel.getKernelSpec().then(spec => {
+    panel.kernelChanged.connect(() => {
+      panel.kernel.getKernelSpec().then(spec => {
         widget.node.textContent = spec.display_name;
       });
     });
@@ -241,7 +241,7 @@ namespace ToolbarItems {
    */
   export
   function createKernelStatusItem(panel: NotebookPanel): Widget {
-    return new KernelIndicator(panel.context);
+    return new KernelIndicator(panel);
   }
 
   /**
@@ -289,7 +289,7 @@ class CellTypeSwitcher extends Widget {
   /**
    * Construct a new cell type switcher.
    */
-  constructor(panel: NotebookPanel) {
+  constructor(content: Notebook) {
     super();
     this.addClass(TOOLBAR_CELLTYPE);
 
@@ -297,39 +297,20 @@ class CellTypeSwitcher extends Widget {
     // Change current cell type on a change in the dropdown.
     select.addEventListener('change', event => {
       if (!this._changeGuard) {
-        NotebookActions.changeCellType(panel.content, select.value);
-      }
-    });
-    // Follow the type of the current cell.
-    panel.content.stateChanged.connect((sender, args) => {
-      if (!panel.model) {
-        return;
-      }
-      if (args.name === 'activeCellIndex') {
-        this._changeGuard = true;
-        select.value = panel.model.cells.get(args.newValue).type;
-        this._changeGuard = false;
+        NotebookActions.changeCellType(content, select.value);
       }
     });
 
-    panel.content.modelChanged.connect(() => {
-      this.followModel(panel);
-    });
-    if (panel.model) {
-      this.followModel(panel);
+    // Set the initial value.
+    let index = content.activeCellIndex;
+    if (content.model) {
+      select.value = content.model.cells.get(index).type;
     }
-  }
 
-  followModel(panel: NotebookPanel): void {
-    let select = this.node.firstChild as HTMLSelectElement;
-    // Set the initial value.
-    let index = panel.content.activeCellIndex;
-    select.value = panel.model.cells.get(index).type;
-    // Follow a change in the cells.
-    panel.content.model.cells.changed.connect((sender, args) => {
-      index = panel.content.activeCellIndex;
+    // Follow the type of the active cell.
+    content.activeCellChanged.connect((sender, cell) => {
       this._changeGuard = true;
-      select.value = panel.model.cells.get(index).type;
+      select.value = cell.model.type;
       this._changeGuard = false;
     });
   }
@@ -345,17 +326,17 @@ class KernelIndicator extends Widget {
   /**
    * Construct a new kernel status widget.
    */
-  constructor(context: IDocumentContext) {
+  constructor(panel: NotebookPanel) {
     super();
     this.addClass(TOOLBAR_INDICATOR);
-    if (context.kernel) {
-      this._handleStatus(context.kernel, context.kernel.status);
-      context.kernel.statusChanged.connect(this._handleStatus, this);
+    if (panel.kernel) {
+      this._handleStatus(panel.kernel, panel.kernel.status);
+      panel.kernel.statusChanged.connect(this._handleStatus, this);
     } else {
       this.addClass(TOOLBAR_BUSY);
       this.node.title = 'No Kernel!';
     }
-    context.kernelChanged.connect((c, kernel) => {
+    panel.kernelChanged.connect((c, kernel) => {
       this._handleStatus(kernel, kernel.status);
       kernel.statusChanged.connect(this._handleStatus, this);
     });

+ 233 - 194
src/notebook/notebook/panel.ts

@@ -5,18 +5,6 @@ import {
   IKernel
 } from 'jupyter-js-services';
 
-import {
-  showDialog
-} from '../../dialog';
-
-import {
-  IDocumentContext
-} from '../../docregistry';
-
-import {
-  RenderMime
-} from '../../rendermime';
-
 import {
   MimeData as IClipboard
 } from 'phosphor-dragdrop';
@@ -29,16 +17,24 @@ import {
   IChangedArgs
 } from 'phosphor-properties';
 
+import {
+  ISignal, Signal
+} from 'phosphor-signaling';
+
 import {
   Widget
 } from 'phosphor-widget';
 
 import {
-  CellEditorWidget, ITextChange, ICompletionRequest
-} from '../cells/editor';
+  IDocumentContext
+} from '../../docregistry';
+
+import {
+  RenderMime
+} from '../../rendermime';
 
 import {
-  CompletionWidget, CompletionModel
+  CompletionWidget, CompletionModel, CellCompletionHandler
 } from '../completion';
 
 import {
@@ -79,99 +75,62 @@ const DIRTY_CLASS = 'jp-mod-dirty';
  */
 export
 class NotebookPanel extends Widget {
-  /**
-   * Create a new content area for the notebook.
-   */
-  static createContent(model: INotebookModel, rendermime: RenderMime<Widget>): Notebook {
-    let widget = new Notebook({ rendermime });
-    widget.model = model;
-    return widget;
-  }
-
-  /**
-   * Create a new toolbar for the notebook.
-   */
-  static createToolbar(): NotebookToolbar {
-    return new NotebookToolbar();
-  }
-
-  /**
-   * Create a new completion widget.
-   */
-  static createCompletion(): CompletionWidget {
-    let model = new CompletionModel();
-    return new CompletionWidget(model);
-  }
-
   /**
    * Construct a new notebook panel.
    */
-  constructor(model: INotebookModel, rendermime: RenderMime<Widget>, context: IDocumentContext, clipboard: IClipboard) {
+  constructor(options: NotebookPanel.IOptions) {
     super();
     this.addClass(NB_PANEL);
-    this._model = model;
-    this._rendermime = rendermime;
-    this._context = context;
-    this._clipboard = clipboard;
-
-    context.kernelChanged.connect(() => {
-      this.handleKernelChange(context.kernel);
-    });
-    if (context.kernel) {
-      this.handleKernelChange(context.kernel);
-    }
+    this._rendermime = options.rendermime;
+    this._clipboard = options.clipboard;
+    this._renderer = options.renderer || NotebookPanel.defaultRenderer;
 
     this.layout = new PanelLayout();
-    let ctor = this.constructor as typeof NotebookPanel;
-    this._content = ctor.createContent(model, rendermime);
-    this._toolbar = ctor.createToolbar();
+    let rendermime = this._rendermime;
+    this._content = this._renderer.createContent({ rendermime });
+    let toolbar = this._renderer.createToolbar();
 
     let container = new Panel();
     container.addClass(NB_CONTAINER);
     container.addChild(this._content);
 
     let layout = this.layout as PanelLayout;
-    layout.addChild(this._toolbar);
+    layout.addChild(toolbar);
     layout.addChild(container);
 
-    // Instantiate tab completion widget.
-    this._completion = ctor.createCompletion();
+    this._completion = this._renderer.createCompletion();
     this._completion.reference = this;
     this._completion.attach(document.body);
-    this._completion.selected.connect(this.onCompletionSelect, this);
-
-    // Connect signals.
-    this._content.stateChanged.connect(this.onContentChanged, this);
-    let cell = this._content.childAt(this._content.activeCellIndex);
-    if (cell) {
-      let editor = cell.editor;
-      editor.textChanged.connect(this.onTextChange, this);
-      editor.completionRequested.connect(this.onCompletionRequest, this);
-    }
 
-    // Handle the document title.
-    this.title.text = context.path.split('/').pop();
-    context.pathChanged.connect((c, path) => {
-      this.title.text = path.split('/').pop();
+    this._completionHandler = new CellCompletionHandler(this._completion);
+    this._completionHandler.activeCell = this._content.activeCell;
+    this._content.activeCellChanged.connect((s, cell) => {
+      this._completionHandler.activeCell = cell;
     });
+  }
 
-    // Handle changes to dirty state.
-    model.stateChanged.connect((m, args) => {
-      if (args.name === 'dirty') {
-        if (args.newValue) {
-          this.title.className += ` ${DIRTY_CLASS}`;
-        } else {
-          this.title.className = this.title.className.replace(DIRTY_CLASS, '');
-        }
-      }
-    });
+  /**
+   * A signal emitted when the panel context changes.
+   */
+  get contextChanged(): ISignal<NotebookPanel, void> {
+    return Private.contextChangedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when the kernel used by the panel changes.
+   */
+  get kernelChanged(): ISignal<NotebookPanel, IKernel> {
+    return Private.kernelChangedSignal.bind(this);
   }
 
   /**
    * Get the toolbar used by the widget.
+   *
+   * #### Notes
+   * This is a read-only property.
    */
   get toolbar(): NotebookToolbar {
-    return this._toolbar;
+    return (this.layout as PanelLayout).childAt(0) as NotebookToolbar;
   }
 
   /**
@@ -184,6 +143,16 @@ class NotebookPanel extends Widget {
     return this._content;
   }
 
+  /**
+   * Get the current kernel used by the panel.
+   *
+   * #### Notes
+   * This is a a read-only property.
+   */
+  get kernel(): IKernel {
+    return this._context ? this._context.kernel : null;
+  }
+
   /**
    * Get the rendermime instance used by the widget.
    *
@@ -194,6 +163,13 @@ class NotebookPanel extends Widget {
     return this._rendermime;
   }
 
+  /**
+   * Get the renderer used by the widget.
+   */
+  get renderer(): NotebookPanel.IRenderer {
+    return this._renderer;
+  }
+
   /**
    * Get the clipboard instance used by the widget.
    *
@@ -205,24 +181,37 @@ class NotebookPanel extends Widget {
   }
 
   /**
-   * Get the model used by the widget.
+   * The model for the widget.
    *
    * #### Notes
    * This is a read-only property.
    */
   get model(): INotebookModel {
-    return this._model;
+    return this._content ? this._content.model : null;
   }
 
   /**
-   * Get the document context for the widget.
+   * The document context for the widget.
    *
    * #### Notes
-   * This is a read-only property.
+   * Changing the context also changes the model on the
+   * `content`.
    */
-  get context(): IDocumentContext {
+  get context(): IDocumentContext<INotebookModel> {
     return this._context;
   }
+  set context(newValue: IDocumentContext<INotebookModel>) {
+    newValue = newValue || null;
+    if (newValue === this._context) {
+      return;
+    }
+    let oldValue = this._context;
+    this._context = newValue;
+    // Trigger private, protected, and public changes.
+    this._onContextChanged(oldValue, newValue);
+    this.onContextChanged(oldValue, newValue);
+    this.contextChanged.emit(void 0);
+  }
 
   /**
    * Dispose of the resources used by the widget.
@@ -232,40 +221,72 @@ class NotebookPanel extends Widget {
       return;
     }
     this._context = null;
-    this._rendermime = null;
     this._content = null;
-    this._toolbar = null;
+    this._rendermime = null;
     this._clipboard = null;
+    this._completionHandler.dispose();
+    this._completionHandler = null;
     this._completion.dispose();
     this._completion = null;
+    this._renderer = null;
     super.dispose();
   }
 
   /**
-   * Restart the kernel on the panel.
+   * Handle a change to the document context.
+   *
+   * #### Notes
+   * The default implementation is a no-op.
    */
-  restart(): Promise<boolean> {
-    let kernel = this.context.kernel;
-    if (!kernel) {
-      return Promise.resolve(false);
+  protected onContextChanged(oldValue: IDocumentContext<INotebookModel>, newValue: IDocumentContext<INotebookModel>): void { }
+
+
+  /**
+   * Handle a change in the model state.
+   */
+  protected onModelStateChanged(sender: INotebookModel, args: IChangedArgs<any>): void {
+    if (args.name === 'dirty') {
+      this._handleDirtyState();
     }
-    return showDialog({
-      title: 'Restart Kernel?',
-      body: 'Do you want to restart the current kernel? All variables will be lost.',
-      host: this.node
-    }).then(result => {
-      if (result.text === 'OK') {
-        return kernel.restart().then(() => { return true; });
-      } else {
-        return false;
+  }
+
+  /**
+   * Handle a change to the document path.
+   */
+  protected onPathChanged(sender: IDocumentContext<INotebookModel>, path: string): void {
+    this.title.text = path.split('/').pop();
+  }
+
+  /**
+   * Handle a change in the context.
+   */
+  private _onContextChanged(oldValue: IDocumentContext<INotebookModel>, newValue: IDocumentContext<INotebookModel>): void {
+    if (oldValue) {
+      oldValue.kernelChanged.disconnect(this._onKernelChanged, this);
+      oldValue.pathChanged.disconnect(this.onPathChanged, this);
+      if (oldValue.model) {
+        oldValue.model.stateChanged.disconnect(this.onModelStateChanged, this);
       }
-    });
+    }
+    let context = newValue;
+    context.kernelChanged.connect(this._onKernelChanged, this);
+    let oldKernel = oldValue ? oldValue.kernel : null;
+    if (context.kernel !== oldKernel) {
+      this._onKernelChanged(this._context, this._context.kernel);
+    }
+    this._content.model = newValue.model;
+    this._handleDirtyState();
+    newValue.model.stateChanged.connect(this.onModelStateChanged, this);
+
+    // Handle the document title.
+    this.onPathChanged(context, context.path);
+    context.pathChanged.connect(this.onPathChanged, this);
   }
 
   /**
    * Handle a change in the kernel by updating the document metadata.
    */
-  protected handleKernelChange(kernel: IKernel): void {
+  private _onKernelChanged(context: IDocumentContext<INotebookModel>, kernel: IKernel): void {
     if (!this.model) {
       return;
     }
@@ -281,113 +302,131 @@ class NotebookPanel extends Widget {
         language: spec.language
       });
     });
+    this._completionHandler.kernel = kernel;
+    this.kernelChanged.emit(kernel);
   }
 
   /**
-   * Handle a change in the content area.
+   * Handle the dirty state of the model.
    */
-  protected onContentChanged(sender: Notebook, args: IChangedArgs<any>): void {
-    switch (args.name) {
-    case 'activeCellIndex':
-      let cell = this._content.childAt(args.oldValue);
-      let editor = cell.editor;
-      editor.textChanged.disconnect(this.onTextChange, this);
-      editor.completionRequested.disconnect(this.onCompletionRequest, this);
-
-      cell = this._content.childAt(args.newValue);
-      editor = cell.editor;
-      editor.textChanged.connect(this.onTextChange, this);
-      editor.completionRequested.connect(this.onCompletionRequest, this);
-      break;
-    default:
-      break;
-    }
-  }
-
-  /**
-   * Handle a text changed signal from an editor.
-   */
-  protected onTextChange(editor: CellEditorWidget, change: ITextChange): void {
+  private _handleDirtyState(): void {
     if (!this.model) {
       return;
     }
-    let line = change.newValue.split('\n')[change.line];
-    let model = this._completion.model;
-    // If last character entered is not whitespace, update completion.
-    if (line[change.ch - 1] && line[change.ch - 1].match(/\S/)) {
-      // If there is currently a completion
-      if (model.original) {
-        model.current = change;
-      }
+    if (this.model.dirty) {
+      this.title.className += ` ${DIRTY_CLASS}`;
     } else {
-      // If final character is whitespace, reset completion.
-      model.options = null;
-      model.original = null;
-      model.cursor = null;
-      return;
+      this.title.className = this.title.className.replace(DIRTY_CLASS, '');
     }
   }
 
+  private _rendermime: RenderMime<Widget> = null;
+  private _context: IDocumentContext<INotebookModel> = null;
+  private _clipboard: IClipboard = null;
+  private _content: Notebook = null;
+  private _renderer: NotebookPanel.IRenderer = null;
+  private _completion: CompletionWidget = null;
+  private _completionHandler: CellCompletionHandler = null;
+}
+
+
+/**
+ * A namespace for `NotebookPanel` statics.
+ */
+export namespace NotebookPanel {
   /**
-   * Handle a completion requested signal from an editor.
+   * An options interface for NotebookPanels.
    */
-  protected onCompletionRequest(editor: CellEditorWidget, change: ICompletionRequest): void {
-    if (!this.model) {
-      return;
-    }
-    let kernel = this.context.kernel;
-    if (!kernel) {
-      return;
-    }
-    let contents = {
-      // Only send the current line of code for completion.
-      code: change.currentValue.split('\n')[change.line],
-      cursor_pos: change.ch
-    };
-    let pendingComplete = ++this._pendingComplete;
-    let model = this._completion.model;
-    kernel.complete(contents).then(value => {
-      // If model has been disposed, bail.
-      if (model.isDisposed) {
-        return;
-      }
-      // If a newer completion requesy has created a pending request, bail.
-      if (pendingComplete !== this._pendingComplete) {
-        return;
-      }
-      // Completion request failures or negative results fail silently.
-      if (value.status !== 'ok') {
-        return;
-      }
-      // Update the model.
-      model.options = value.matches;
-      model.cursor = { start: value.cursor_start, end: value.cursor_end };
-    }).then(() => {
-      model.original = change;
-    });
+  export
+  interface IOptions {
+    /**
+     * The rendermime instance used by the panel.
+     */
+    rendermime: RenderMime<Widget>;
+
+    /**
+     * The application clipboard.
+     */
+    clipboard: IClipboard;
+
+    /**
+     * The content renderer for the panel.
+     *
+     * The default is a shared `IRenderer` instance.
+     */
+    renderer?: IRenderer;
   }
 
   /**
-   * Handle a completion selected signal from the completion widget.
+   * A renderer interface for NotebookPanels.
    */
-  protected onCompletionSelect(widget: CompletionWidget, value: string): void {
-    if (!this.model) {
-      return;
+  export
+  interface IRenderer {
+    /**
+     * Create a new content area for the panel.
+     */
+    createContent(options: Notebook.IOptions): Notebook;
+
+    /**
+     * Create a new toolbar for the panel.
+     */
+    createToolbar(): NotebookToolbar;
+
+    /**
+     * Create a new completion widget for the panel.
+     */
+    createCompletion(): CompletionWidget;
+  }
+
+  /**
+   * The default implementation of an `IRenderer`.
+   */
+  export
+  class Renderer implements IRenderer {
+    /**
+     * Create a new content area for the panel.
+     */
+    createContent(options: Notebook.IOptions): Notebook {
+      return new Notebook(options);
+    }
+
+    /**
+     * Create a new toolbar for the panel.
+     */
+    createToolbar(): NotebookToolbar {
+      return new NotebookToolbar();
+    }
+
+    /**
+     * Create a new completion widget.
+     */
+    createCompletion(): CompletionWidget {
+      let model = new CompletionModel();
+      return new CompletionWidget(model);
     }
-    let patch = this._completion.model.createPatch(value);
-    let cell = this._content.childAt(this._content.activeCellIndex);
-    let editor = cell.editor.editor;
-    let doc = editor.getDoc();
-    doc.setValue(patch.text);
-    doc.setCursor(doc.posFromIndex(patch.position));
   }
 
-  private _rendermime: RenderMime<Widget> = null;
-  private _context: IDocumentContext = null;
-  private _model: INotebookModel = null;
-  private _content: Notebook = null;
-  private _toolbar: NotebookToolbar = null;
-  private _clipboard: IClipboard = null;
-  private _completion: CompletionWidget = null;
-  private _pendingComplete = 0;
+  /**
+   * The shared default instance of a `Renderer`.
+   */
+   export
+   const defaultRenderer = new Renderer();
+}
+
+
+/**
+ * A namespace for private data.
+ */
+namespace Private {
+  /**
+   * A signal emitted when the panel context changes.
+   */
+  export
+  const contextChangedSignal = new Signal<NotebookPanel, void>();
+
+  /**
+   * A signal emitted when the kernel used by the panel changes.
+   */
+  export
+  const kernelChangedSignal = new Signal<NotebookPanel, IKernel>();
 }

+ 238 - 71
src/notebook/notebook/widget.ts

@@ -30,7 +30,7 @@ import {
 } from 'phosphor-signaling';
 
 import {
-  ChildMessage, Widget
+  Widget
 } from 'phosphor-widget';
 
 import {
@@ -129,6 +129,16 @@ class StaticNotebook extends Widget {
     return Private.modelChangedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when the model content changes.
+   *
+   * #### Notes
+   * This is a convenience signal that follows the current model.
+   */
+  get modelContentChanged(): ISignal<StaticNotebook, void> {
+    return Private.modelContentChanged.bind(this);
+  }
+
   /**
    * The model for the widget.
    */
@@ -220,20 +230,55 @@ class StaticNotebook extends Widget {
     // No-op.
   }
 
+  /**
+   * Handle changes to the notebook model content.
+   *
+   * #### Notes
+   * The default implementation emits the `modelContentChanged` signal.
+   */
+  protected onModelContentChanged(model: INotebookModel, args: void): void {
+    this.modelContentChanged.emit(void 0);
+  }
+
   /**
    * Handle changes to the notebook model metadata.
+   *
+   * #### Notes
+   * The default implementation updates the mimetypes of the code cells
+   * when the `language_info` metadata changes.
    */
   protected onMetadataChanged(model: INotebookModel, args: IChangedArgs<any>): void {
     switch (args.name) {
     case 'language_info':
       this._mimetype = this._renderer.getCodeMimetype(model);
-      this._updateChildren();
+      this._updateCells();
       break;
     default:
       break;
     }
   }
 
+  /**
+   * Handle a cell being inserted.
+   *
+   * The default implementation is a no-op
+   */
+  protected onCellInserted(index: number, cell: BaseCellWidget): void { }
+
+  /**
+   * Handle a cell being moved.
+   *
+   * The default implementation is a no-op
+   */
+  protected onCellMoved(fromIndex: number, toIndex: number): void { }
+
+  /**
+   * Handle a cell being removed.
+   *
+   * The default implementation is a no-op
+   */
+  protected onCellRemoved(cell: BaseCellWidget): void { }
+
   /**
    * Handle a new model on the widget.
    */
@@ -242,9 +287,10 @@ class StaticNotebook extends Widget {
     if (oldValue) {
       oldValue.cells.changed.disconnect(this._onCellsChanged, this);
       oldValue.metadataChanged.disconnect(this.onMetadataChanged, this);
+      oldValue.contentChanged.disconnect(this.onModelContentChanged, this);
       // TODO: reuse existing cell widgets if possible.
       for (let i = 0; i < layout.childCount(); i++) {
-        this._removeChild(0);
+        this._removeCell(0);
       }
     }
     if (!newValue) {
@@ -254,16 +300,52 @@ class StaticNotebook extends Widget {
     this._mimetype = this._renderer.getCodeMimetype(newValue);
     let cells = newValue.cells;
     for (let i = 0; i < cells.length; i++) {
-      this._insertChild(i, cells.get(i));
+      this._insertCell(i, cells.get(i));
     }
     cells.changed.connect(this._onCellsChanged, this);
+    newValue.contentChanged.connect(this.onModelContentChanged, this);
     newValue.metadataChanged.connect(this.onMetadataChanged, this);
   }
 
   /**
-   * Create a child widget and insert into to the notebook.
+   * Handle a change cells event.
    */
-  private _insertChild(index: number, cell: ICellModel): void {
+  private _onCellsChanged(sender: IObservableList<ICellModel>, args: IListChangedArgs<ICellModel>) {
+    switch (args.type) {
+    case ListChangeType.Add:
+      this._insertCell(args.newIndex, args.newValue as ICellModel);
+      break;
+    case ListChangeType.Move:
+      this._moveCell(args.newIndex, args.oldIndex);
+      break;
+    case ListChangeType.Remove:
+      this._removeCell(args.oldIndex);
+      break;
+    case ListChangeType.Replace:
+      // TODO: reuse existing cell widgets if possible.
+      let oldValues = args.oldValue as ICellModel[];
+      for (let i = 0; i < oldValues.length; i++) {
+        this._removeCell(args.oldIndex);
+      }
+      let newValues = args.newValue as ICellModel[];
+      for (let i = newValues.length; i > 0; i--) {
+        this._insertCell(args.newIndex, newValues[i - 1]);
+      }
+      break;
+    case ListChangeType.Set:
+      // TODO: reuse existing widget if possible.
+      this._removeCell(args.newIndex);
+      this._insertCell(args.newIndex, args.newValue as ICellModel);
+      break;
+    default:
+      return;
+    }
+  }
+
+  /**
+   * Create a cell widget and insert into the notebook.
+   */
+  private _insertCell(index: number, cell: ICellModel): void {
     let widget: BaseCellWidget;
     switch (cell.type) {
     case 'code':
@@ -280,73 +362,50 @@ class StaticNotebook extends Widget {
     widget.addClass(NB_CELL_CLASS);
     let layout = this.layout as PanelLayout;
     layout.insertChild(index, widget);
-    this._updateChild(index);
+    this._updateCell(index);
+    this.onCellInserted(index, widget);
   }
 
   /**
-   * Update the child widgets.
+   * Move a cell widget.
    */
-  private _updateChildren(): void {
+  private _moveCell(fromIndex: number, toIndex: number): void {
     let layout = this.layout as PanelLayout;
-    for (let i = 0; i < layout.childCount(); i++) {
-      this._updateChild(i);
-    }
+    layout.insertChild(toIndex, layout.childAt(fromIndex));
+    this.onCellMoved(fromIndex, toIndex);
   }
 
   /**
-   * Update a child widget.
+   * Remove a cell widget.
    */
-  private _updateChild(index: number): void {
+  private _removeCell(index: number): void {
     let layout = this.layout as PanelLayout;
-    let child = layout.childAt(index) as BaseCellWidget;
-    if (child instanceof CodeCellWidget) {
-      child.mimetype = this._mimetype;
-    }
-    this._renderer.updateCell(child);
+    let widget = layout.childAt(index) as BaseCellWidget;
+    widget.parent = null;
+    this.onCellRemoved(widget);
+    widget.dispose();
   }
 
   /**
-   * Remove a child widget.
+   * Update the cell widgets.
    */
-  private _removeChild(index: number): void {
+  private _updateCells(): void {
     let layout = this.layout as PanelLayout;
-    layout.childAt(index).dispose();
+    for (let i = 0; i < layout.childCount(); i++) {
+      this._updateCell(i);
+    }
   }
 
   /**
-   * Handle a change cells event.
+   * Update a cell widget.
    */
-  private _onCellsChanged(sender: IObservableList<ICellModel>, args: IListChangedArgs<ICellModel>) {
+  private _updateCell(index: number): void {
     let layout = this.layout as PanelLayout;
-    switch (args.type) {
-    case ListChangeType.Add:
-      this._insertChild(args.newIndex, args.newValue as ICellModel);
-      break;
-    case ListChangeType.Move:
-      layout.insertChild(args.newIndex, layout.childAt(args.oldIndex));
-      break;
-    case ListChangeType.Remove:
-      this._removeChild(args.oldIndex);
-      break;
-    case ListChangeType.Replace:
-      // TODO: reuse existing cell widgets if possible.
-      let oldValues = args.oldValue as ICellModel[];
-      for (let i = 0; i < oldValues.length; i++) {
-        this._removeChild(args.oldIndex);
-      }
-      let newValues = args.newValue as ICellModel[];
-      for (let i = newValues.length; i > 0; i--) {
-        this._insertChild(args.newIndex, newValues[i - 1]);
-      }
-      break;
-    case ListChangeType.Set:
-      // TODO: reuse existing widget if possible.
-      this._removeChild(args.newIndex);
-      this._insertChild(args.newIndex, args.newValue as ICellModel);
-      break;
-    default:
-      return;
+    let child = layout.childAt(index) as BaseCellWidget;
+    if (child instanceof CodeCellWidget) {
+      child.mimetype = this._mimetype;
     }
+    this._renderer.updateCell(child);
   }
 
   private _mimetype = 'text/plain';
@@ -491,6 +550,17 @@ class Notebook extends StaticNotebook {
     return Private.stateChangedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when the active cell changes.
+   *
+   * #### Notes
+   * This can be due to the active index changing or the
+   * cell at the active index changing.
+   */
+  get activeCellChanged(): ISignal<StaticNotebook, BaseCellWidget> {
+    return Private.activeCellChangedSignal.bind(this);
+  }
+
   /**
    * The interactivity mode of the notebook.
    */
@@ -522,23 +592,53 @@ class Notebook extends StaticNotebook {
    * The index will be clamped to the bounds of the notebook cells.
    */
   get activeCellIndex(): number {
+    if (!this.model) {
+      return -1;
+    }
     return this.model.cells.length ? this._activeCellIndex : -1;
   }
   set activeCellIndex(newValue: number) {
-    if (!this.model.cells.length) {
-      return;
+    let oldValue = this._activeCellIndex;
+    if (!this.model || !this.model.cells.length) {
+      newValue = -1;
+    } else {
+      newValue = Math.max(newValue, 0);
+      newValue = Math.min(newValue, this.model.cells.length - 1);
+    }
+    this._activeCellIndex = newValue;
+    let cell = this.childAt(newValue);
+    if (cell !== this._activeCell) {
+      this._activeCell = cell;
+      this.activeCellChanged.emit(cell);
     }
-    newValue = Math.max(newValue, 0);
-    newValue = Math.min(newValue, this.model.cells.length - 1);
-    if (newValue === this._activeCellIndex) {
+    if (newValue === oldValue) {
       return;
     }
-    let oldValue = this._activeCellIndex;
-    this._activeCellIndex = newValue;
     this.stateChanged.emit({ name: 'activeCellIndex', oldValue, newValue });
     this.update();
   }
 
+  /**
+   * Get the active cell widget.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get activeCell(): BaseCellWidget {
+    return this._activeCell;
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._activeCell = null;
+    super.dispose();
+  }
+
   /**
    * Select a cell widget.
    */
@@ -665,21 +765,42 @@ class Notebook extends StaticNotebook {
   }
 
   /**
-   * Handle a `child-added` message.
+   * Handle a cell being inserted.
    */
-  protected onChildAdded(msg: ChildMessage): void {
-    let widget = msg.child as BaseCellWidget;
-    widget.editor.edgeRequested.connect(this._onEdgeRequest, this);
+  protected onCellInserted(index: number, cell: BaseCellWidget): void {
+    cell.editor.edgeRequested.connect(this._onEdgeRequest, this);
+    // Trigger an update of the active cell.
+    this.activeCellIndex = this.activeCellIndex;
     this.update();
   }
 
   /**
-   * Handle a `child-removed` message.
+   * Handle a cell being moved.
+   */
+  protected onCellMoved(fromIndex: number, toIndex: number): void {
+    if (fromIndex === this.activeCellIndex) {
+      this.activeCellIndex = toIndex;
+    }
+  }
+
+  /**
+   * Handle a cell being removed.
    */
-  protected onChildRemoved(msg: ChildMessage): void {
+  protected onCellRemoved(cell: BaseCellWidget): void {
+    // Trigger an update of the active cell.
+    this.activeCellIndex = this.activeCellIndex;
     this.update();
   }
 
+  /**
+   * Handle a new model.
+   */
+  protected onModelChanged(oldValue: INotebookModel, newValue: INotebookModel): void {
+    // Try to set the active cell index to 0.
+    // It will be set to `-1` if there is no new model or the model is empty.
+    this.activeCellIndex = 0;
+  }
+
   /**
    * Handle edge request signals from cells.
    */
@@ -720,7 +841,7 @@ class Notebook extends StaticNotebook {
    */
   private _evtClick(event: MouseEvent): void {
     let model = this.model;
-    if (model.readOnly) {
+    if (!model || model.readOnly) {
       return;
     }
     let i = this._findCell(event.target as HTMLElement);
@@ -735,7 +856,7 @@ class Notebook extends StaticNotebook {
    */
   private _evtDblClick(event: MouseEvent): void {
     let model = this.model;
-    if (model.readOnly) {
+    if (!model || model.readOnly) {
       return;
     }
     let i = this._findCell(event.target as HTMLElement);
@@ -754,15 +875,46 @@ class Notebook extends StaticNotebook {
    * Handle `focus` events for the widget.
    */
   private _evtFocus(event: FocusEvent): void {
-    if (event.target === this.node) {
-      this.mode = 'command';
-    } else {
+    this.mode = 'command';
+    let i = this._findCell(event.target as HTMLElement);
+    if (i === -1) {
+      return;
+    }
+    this.activeCellIndex = i;
+    let widget = this.childAt(i);
+    if (widget.editor.node.contains(event.target as HTMLElement)) {
       this.mode = 'edit';
     }
   }
 
   private _mode: NotebookMode = 'command';
-  private _activeCellIndex = 0;
+  private _activeCellIndex = -1;
+  private _activeCell: BaseCellWidget = null;
+}
+
+
+/**
+ * The namespace for the `Notebook` class statics.
+ */
+export
+namespace Notebook {
+  /**
+   * An options object for initializing a notebook.
+   */
+  export
+  interface IOptions extends StaticNotebook.IOptions { }
+
+  /**
+   * The default implementation of an `IRenderer`.
+   */
+  export
+  class Renderer extends StaticNotebook.Renderer { }
+
+  /**
+   * The default `IRenderer` instance.
+   */
+  export
+  const defaultRenderer = new Renderer();
 }
 
 
@@ -785,6 +937,21 @@ namespace Private {
   export
   const modelChangedSignal = new Signal<StaticNotebook, void>();
 
+  /**
+   * A signal emitted when the model content changes.
+   */
+  export
+  const modelContentChanged = new Signal<StaticNotebook, void>();
+
+  /**
+   * A signal emitted when the active cell changes.
+   *
+   * #### Notes
+   * This can be due to the active index changing or the
+   * cell at the active index changing.
+   */
+  export
+  const activeCellChangedSignal = new Signal<StaticNotebook, BaseCellWidget>();
 
   /**
    * A signal emitted when the state changes on the notebook.

+ 6 - 4
src/notebook/notebook/widgetfactory.ts

@@ -38,7 +38,7 @@ import {
  * A widget factory for notebook panels.
  */
 export
-class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel> {
+class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel, INotebookModel> {
   /**
    * Construct a new notebook widget factory.
    *
@@ -76,15 +76,17 @@ class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel> {
    * The factory will start the appropriate kernel and populate
    * the default toolbar items using `ToolbarItems.populateDefaults`.
    */
-  createNew(model: INotebookModel, context: IDocumentContext, kernel?: IKernelId): NotebookPanel {
+  createNew(context: IDocumentContext<INotebookModel>, kernel?: IKernelId): NotebookPanel {
     let rendermime = this._rendermime.clone();
+    let model = context.model;
     if (kernel) {
       context.changeKernel(kernel);
     } else {
       let name = findKernel(model.defaultKernelName, model.defaultKernelLanguage, context.kernelspecs);
       context.changeKernel({ name });
     }
-    let panel = new NotebookPanel(model, rendermime, context, this._clipboard);
+    let panel = new NotebookPanel({ rendermime, clipboard: this._clipboard });
+    panel.context = context;
     ToolbarItems.populateDefaults(panel);
     return panel;
   }
@@ -97,7 +99,7 @@ class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel> {
    *
    * ### The default implementation is a no-op.
    */
-  beforeClose(model: INotebookModel, context: IDocumentContext, widget: NotebookPanel): Promise<boolean> {
+  beforeClose(widget: NotebookPanel, context: IDocumentContext<INotebookModel>): Promise<boolean> {
     // No special action required.
     return Promise.resolve(true);
   }

+ 7 - 5
src/notebook/plugin.ts

@@ -173,8 +173,8 @@ class TrackingNotebookWidgetFactory extends NotebookWidgetFactory {
   /**
    * Create a new widget.
    */
-  createNew(model: INotebookModel, context: IDocumentContext, kernel?: IKernelId): NotebookPanel {
-    let widget = super.createNew(model, context, kernel);
+  createNew(context: IDocumentContext<INotebookModel>, kernel?: IKernelId): NotebookPanel {
+    let widget = super.createNew(context, kernel);
     Private.notebookTracker.activeNotebook = widget;
     return widget;
   }
@@ -261,7 +261,7 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
     handler: () => {
       if (tracker.activeNotebook) {
         let nbWidget = tracker.activeNotebook;
-        nbWidget.restart();
+        NotebookActions.restart(nbWidget.kernel, nbWidget.node);
       }
     }
   },
@@ -270,7 +270,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
     handler: () => {
       if (tracker.activeNotebook) {
         let nbWidget = tracker.activeNotebook;
-        nbWidget.restart().then(result => {
+        let promise = NotebookActions.restart(nbWidget.kernel, nbWidget.node);
+        promise.then(result => {
           if (result) {
             NotebookActions.clearAllOutputs(nbWidget.content);
           }
@@ -283,7 +284,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
     handler: () => {
       if (tracker.activeNotebook) {
         let nbWidget = tracker.activeNotebook;
-        nbWidget.restart().then(result => {
+        let promise = NotebookActions.restart(nbWidget.kernel, nbWidget.node);
+        promise.then(result => {
           NotebookActions.runAll(nbWidget.content, nbWidget.context.kernel);
         });
       }

+ 6 - 6
src/widgets/index.ts

@@ -28,7 +28,7 @@ import {
 } from '../rendermime';
 
 import {
-  IDocumentContext
+  IDocumentContext, IDocumentModel
 } from '../docregistry';
 
 import 'jquery-ui/themes/smoothness/jquery-ui.min.css';
@@ -69,8 +69,8 @@ class BackboneViewWrapper extends Widget {
  */
 export
 class WidgetManager extends ManagerBase<Widget> implements IDisposable {
-  constructor(context: IDocumentContext) {
-    super()
+  constructor(context: IDocumentContext<IDocumentModel>) {
+    super();
     this._context = context;
 
     let newKernel = (kernel: IKernel) => {
@@ -79,12 +79,12 @@ class WidgetManager extends ManagerBase<Widget> implements IDisposable {
         }
         this._commRegistration = kernel.registerCommTarget(this.comm_target_name,
         (comm, msg) => {this.handle_comm_open(comm, msg)});
-    }
+    };
 
     context.kernelChanged.connect((sender, kernel) => {
       this.validateVersion();
       newKernel(kernel);
-    })
+    });
 
     if (context.kernel) {
       this.validateVersion();
@@ -150,7 +150,7 @@ class WidgetManager extends ManagerBase<Widget> implements IDisposable {
     this._context = null;
   }
 
-  _context: IDocumentContext;
+  _context: IDocumentContext<IDocumentModel>;
   _commRegistration: IDisposable;
 }
 

+ 8 - 5
src/widgets/plugin.ts

@@ -13,6 +13,10 @@ import {
   Widget
 } from 'phosphor-widget';
 
+import {
+  INotebookModel
+} from '../notebook/notebook/model';
+
 import {
   NotebookPanel
 } from '../notebook/notebook/panel';
@@ -43,21 +47,20 @@ const widgetManagerExtension = {
 };
 
 export
-class IPyWidgetExtension implements IWidgetExtension<NotebookPanel>{
+class IPyWidgetExtension implements IWidgetExtension<NotebookPanel, INotebookModel> {
   /**
    * Create a new extension object.
    */
-  createNew(nb: NotebookPanel, model: IDocumentModel,
-            context: IDocumentContext): IDisposable {
+  createNew(nb: NotebookPanel, context: IDocumentContext<INotebookModel>): IDisposable {
     let wManager = new WidgetManager(context);
     let wRenderer = new WidgetRenderer(wManager);
 
-    nb.content.rendermime.addRenderer(WIDGET_MIMETYPE, wRenderer, 0)
+    nb.content.rendermime.addRenderer(WIDGET_MIMETYPE, wRenderer, 0);
     return new DisposableDelegate(() => {
       nb.content.rendermime.removeRenderer(WIDGET_MIMETYPE);
       wRenderer.dispose();
       wManager.dispose();
-    })
+    });
   }
 }
 

+ 14 - 11
test/src/docmanager/mockcontext.ts

@@ -85,21 +85,21 @@ const LANGUAGE_INFOS: { [key: string]: IKernelLanguageInfo } = {
 
 
 export
-class MockContext implements IDocumentContext {
+class MockContext<T extends IDocumentModel> implements IDocumentContext<T> {
 
-  constructor(model: IDocumentModel) {
+  constructor(model: T) {
     this._model = model;
   }
 
-  get kernelChanged(): ISignal<MockContext, IKernel> {
+  get kernelChanged(): ISignal<IDocumentContext<IDocumentModel>, IKernel> {
     return Private.kernelChangedSignal.bind(this);
   }
 
-  get pathChanged(): ISignal<MockContext, string> {
+  get pathChanged(): ISignal<IDocumentContext<IDocumentModel>, string> {
     return Private.pathChangedSignal.bind(this);
   }
 
-  get dirtyCleared(): ISignal<MockContext, void> {
+  get dirtyCleared(): ISignal<IDocumentContext<IDocumentModel>, void> {
     return Private.dirtyClearedSignal.bind(this);
   }
 
@@ -107,7 +107,7 @@ class MockContext implements IDocumentContext {
     return '';
   }
 
-  get model(): IDocumentModel {
+  get model(): T {
     return this._model;
   }
 
@@ -116,7 +116,7 @@ class MockContext implements IDocumentContext {
   }
 
   get path(): string {
-    return '';
+    return this._path;
   }
 
   get contentsModel(): IContentsModel {
@@ -164,6 +164,8 @@ class MockContext implements IDocumentContext {
   }
 
   saveAs(path: string): Promise<void> {
+    this._path = path;
+    this.pathChanged.emit(path);
     return Promise.resolve(void 0);
   }
 
@@ -179,7 +181,8 @@ class MockContext implements IDocumentContext {
     return void 0;
   }
 
-  private _model: IDocumentModel = null;
+  private _model: T = null;
+  private _path = '';
   private _kernel: IKernel = null;
 }
 
@@ -194,17 +197,17 @@ namespace Private {
    * A signal emitted when the kernel changes.
    */
   export
-  const kernelChangedSignal = new Signal<MockContext, IKernel>();
+  const kernelChangedSignal = new Signal<IDocumentContext<IDocumentModel>, IKernel>();
 
   /**
    * A signal emitted when the path changes.
    */
   export
-  const pathChangedSignal = new Signal<MockContext, string>();
+  const pathChangedSignal = new Signal<IDocumentContext<IDocumentModel>, string>();
 
   /**
    * A signal emitted when the model is saved or reverted.
    */
   export
-  const dirtyClearedSignal = new Signal<MockContext, void>();
+  const dirtyClearedSignal = new Signal<IDocumentContext<IDocumentModel>, void>();
 }

+ 4 - 4
test/src/docregistry/default.spec.ts

@@ -23,7 +23,7 @@ import {
 
 class WidgetFactory extends ABCWidgetFactory {
 
-  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): Widget {
+  createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernelId): Widget {
     return new Widget();
   }
 }
@@ -72,7 +72,7 @@ describe('docmanager/default', () => {
         let factory = new WidgetFactory();
         let model = new DocumentModel();
         let context = new MockContext(model);
-        let widget = factory.createNew(model, context);
+        let widget = factory.createNew(context);
         expect(widget).to.be.a(Widget);
       });
 
@@ -84,8 +84,8 @@ describe('docmanager/default', () => {
         let factory = new WidgetFactory();
         let model = new DocumentModel();
         let context = new MockContext(model);
-        let widget = factory.createNew(model, context);
-        factory.beforeClose(model, context, widget).then(() => {
+        let widget = factory.createNew(context);
+        factory.beforeClose(widget, context).then(() => {
           done();
         });
       });

+ 1 - 0
test/src/index.ts

@@ -12,6 +12,7 @@ import './notebook/cells/model.spec';
 import './notebook/notebook/nbformat.spec';
 import './notebook/notebook/model.spec';
 import './notebook/notebook/modelfactory.spec';
+import './notebook/notebook/panel.spec';
 import './notebook/notebook/toolbar.spec';
 import './notebook/notebook/trust.spec';
 import './notebook/notebook/widget.spec';

+ 430 - 0
test/src/notebook/notebook/panel.spec.ts

@@ -0,0 +1,430 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  IKernel
+} from 'jupyter-js-services';
+
+import {
+  MockKernel
+} from 'jupyter-js-services/lib/mockkernel';
+
+import {
+  MimeData
+} from 'phosphor-dragdrop';
+
+import {
+  IChangedArgs
+} from 'phosphor-properties';
+
+import {
+  IDocumentContext
+} from '../../../../lib/docregistry';
+
+import {
+  CellEditorWidget, ITextChange, ICompletionRequest
+} from '../../../../lib/notebook/cells/editor';
+
+import {
+  CompletionWidget
+} from '../../../../lib/notebook/completion';
+
+import {
+  INotebookModel, NotebookModel
+} from '../../../../lib/notebook/notebook/model';
+
+import {
+  nbformat
+} from '../../../../lib/notebook/notebook/nbformat';
+
+import {
+  NotebookPanel
+} from '../../../../lib/notebook/notebook/panel';
+
+import {
+  NotebookToolbar
+} from '../../../../lib/notebook/notebook/toolbar';
+
+import {
+  Notebook
+} from '../../../../lib/notebook/notebook/widget';
+
+import {
+  MockContext
+} from '../../docmanager/mockcontext';
+
+import {
+  defaultRenderMime
+} from '../../rendermime/rendermime.spec';
+
+
+/**
+ * Default data.
+ */
+const rendermime = defaultRenderMime();
+const clipboard = new MimeData();
+const DEFAULT_CONTENT: nbformat.INotebookContent = require('../../../../examples/notebook/test.ipynb') as nbformat.INotebookContent;
+
+
+class LogNotebookPanel extends NotebookPanel {
+
+  methods: string[] = [];
+
+  protected onContextChanged(oldValue: IDocumentContext<INotebookModel>, newValue: IDocumentContext<INotebookModel>): void {
+    super.onContextChanged(oldValue, newValue);
+    this.methods.push('onContextChanged');
+  }
+
+  protected onModelStateChanged(sender: INotebookModel, args: IChangedArgs<any>): void {
+    super.onModelStateChanged(sender, args);
+    this.methods.push('onModelStateChanged');
+  }
+
+  protected onPathChanged(sender: IDocumentContext<INotebookModel>, path: string): void {
+    super.onPathChanged(sender, path);
+    this.methods.push('onPathChanged');
+  }
+}
+
+
+function createPanel(): LogNotebookPanel {
+  let panel = new LogNotebookPanel({ rendermime, clipboard });
+  let model = new NotebookModel();
+  model.fromJSON(DEFAULT_CONTENT);
+  let context = new MockContext<NotebookModel>(model);
+  panel.context = context;
+  return panel;
+}
+
+
+describe('notebook/notebook/panel', () => {
+
+  describe('NotebookPanel', () => {
+
+    describe('#constructor()', () => {
+
+      it('should create a notebook panel', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel).to.be.a(NotebookPanel);
+      });
+
+
+      it('should accept an optional render', () => {
+        let renderer = new NotebookPanel.Renderer();
+        let panel = new NotebookPanel({ rendermime, clipboard, renderer });
+        expect(panel.renderer).to.be(renderer);
+      });
+
+    });
+
+    describe('#contextChanged', () => {
+
+      it('should be emitted when the context on the panel changes', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        let called = false;
+        let model = new NotebookModel();
+        let context = new MockContext<INotebookModel>(model);
+        panel.contextChanged.connect((sender, args) => {
+          expect(sender).to.be(panel);
+          expect(args).to.be(void 0);
+          called = true;
+        });
+        panel.context = context;
+        expect(called).to.be(true);
+      });
+
+      it('should not be emitted if the context does not change', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        let called = false;
+        let model = new NotebookModel();
+        let context = new MockContext<INotebookModel>(model);
+        panel.context = context;
+        panel.contextChanged.connect(() => { called = true; });
+        panel.context = context;
+        expect(called).to.be(false);
+      });
+
+    });
+
+    describe('#kernelChanged', () => {
+
+      it('should be emitted when the kernel on the panel changes', () => {
+        let panel = createPanel();
+        let called = false;
+        panel.kernelChanged.connect((sender, args) => {
+          expect(sender).to.be(panel);
+          expect(args).to.be.a(MockKernel);
+          called = true;
+        });
+        panel.context.changeKernel({ name: 'python' });
+        expect(called).to.be(true);
+      });
+
+      it('should not be emitted when the kernel does not change', () => {
+        let panel = createPanel();
+        let called = false;
+        panel.kernelChanged.connect(() => { called = true; });
+        let context = new MockContext<INotebookModel>(panel.model);
+        panel.context = context;
+        expect(called).to.be(false);
+      });
+
+    });
+
+    describe('#toolbar', () => {
+
+      it('should be the toolbar used by the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.toolbar).to.be.a(NotebookToolbar);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.toolbar = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#content', () => {
+
+      it('should be the content area used by the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.content).to.be.a(Notebook);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.content = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#kernel', () => {
+
+      it('should be the current kernel used by the panel', () => {
+        let panel = createPanel();
+        expect(panel.kernel).to.be(null);
+        panel.context.changeKernel({ name: 'python' });
+        expect(panel.kernel.name).to.be('python');
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.kernel = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#rendermime', () => {
+
+      it('should be the rendermime instance used by the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.rendermime).to.be(rendermime);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.rendermime = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#renderer', () => {
+
+      it('should be the renderer used by the widget', () => {
+        let renderer = new NotebookPanel.Renderer();
+        let panel = new NotebookPanel({ rendermime, clipboard, renderer });
+        expect(panel.renderer).to.be(renderer);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.renderer = null; });
+      });
+
+    });
+
+    describe('#clipboard', () => {
+
+      it('should be the clipboard instance used by the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.clipboard).to.be(clipboard);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.clipboard = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#model', () => {
+
+      it('should be the model for the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.model).to.be(null);
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.context = context;
+        expect(panel.model).to.be(model);
+        expect(panel.content.model).to.be(model);
+      });
+
+      it('should be read-only', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(() => { panel.model = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#context', () => {
+
+      it('should get the document context for the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        expect(panel.context).to.be(null);
+      });
+
+      it('should set the document context for the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.context = context;
+        expect(panel.context).to.be(context);
+      });
+
+      it('should emit the `contextChanged` signal', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        let called = false;
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.contextChanged.connect(() => { called = true; });
+        panel.context = context;
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#dispose()', () => {
+
+      it('should dispose of the resources used by the widget', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.context = context;
+        panel.dispose();
+        expect(panel.isDisposed).to.be(true);
+      });
+
+      it('should be safe to call more than once', () => {
+        let panel = new NotebookPanel({ rendermime, clipboard });
+        panel.dispose();
+        panel.dispose();
+        expect(panel.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#onContextChanged()', () => {
+
+      it('should be called when the context changes', () => {
+        let panel = new LogNotebookPanel({ rendermime, clipboard });
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.methods = [];
+        panel.context = context;
+        expect(panel.methods).to.contain('onContextChanged');
+      });
+
+    });
+
+    describe('#onModelStateChanged()', () => {
+
+      it('should be called when the model state changes', () => {
+        let panel = createPanel();
+        panel.methods = [];
+        panel.model.dirty = false;
+        expect(panel.methods).to.contain('onModelStateChanged');
+      });
+
+      it('should update the title className based on the dirty state', () => {
+        let panel = createPanel();
+        panel.model.dirty = true;
+        expect(panel.title.className).to.contain('jp-mod-dirty');
+        panel.model.dirty = false;
+        expect(panel.title.className).to.not.contain('jp-mod-dirty');
+      });
+
+    });
+
+    describe('#onPathChanged()', () => {
+
+      it('should be called when the path changes', () => {
+        let panel = createPanel();
+        panel.methods = [];
+        panel.context.saveAs('foo.ipynb');
+        expect(panel.methods).to.contain('onPathChanged');
+      });
+
+      it('should be called when the context changes', () => {
+        let panel = new LogNotebookPanel({ rendermime, clipboard });
+        let model = new NotebookModel();
+        let context = new MockContext<NotebookModel>(model);
+        panel.methods = [];
+        panel.context = context;
+        expect(panel.methods).to.contain('onPathChanged');
+      });
+
+      it('should update the title text', () => {
+        let panel = createPanel();
+        panel.methods = [];
+        panel.context.saveAs('test/foo.ipynb');
+        expect(panel.methods).to.contain('onPathChanged');
+        expect(panel.title.text).to.be('foo.ipynb');
+      });
+
+    });
+
+    describe('.Renderer', () => {
+
+      describe('#createContent()', () => {
+
+        it('should create a notebook widget', () => {
+          let renderer = new NotebookPanel.Renderer();
+          expect(renderer.createContent({ rendermime })).to.be.a(Notebook);
+        });
+
+      });
+
+      describe('#createToolbar()', () => {
+
+        it('should create a notebook toolbar', () => {
+          let renderer = new NotebookPanel.Renderer();
+          expect(renderer.createToolbar()).to.be.a(NotebookToolbar);
+        });
+
+      });
+
+      describe('#createCompletion()', () => {
+
+        it('should create a completion widget', () => {
+          let renderer = new NotebookPanel.Renderer();
+          expect(renderer.createCompletion()).to.be.a(CompletionWidget);
+        });
+
+      });
+
+    });
+
+    describe('.defaultRenderer', () => {
+
+      it('should be an instance of a `Renderer`', () => {
+        expect(NotebookPanel.defaultRenderer).to.be.a(NotebookPanel.Renderer);
+      });
+
+    });
+
+  });
+
+});

+ 169 - 15
test/src/notebook/notebook/widget.spec.ts

@@ -11,17 +11,13 @@ import {
   IChangedArgs
 } from 'phosphor-properties';
 
-import {
-  ChildMessage
-} from 'phosphor-widget';
-
 import {
   simulate
 } from 'simulate-event';
 
 import {
   CodeCellModel, CodeCellWidget, MarkdownCellModel, MarkdownCellWidget,
-  RawCellModel, RawCellWidget
+  RawCellModel, RawCellWidget, BaseCellWidget
 } from '../../../../lib/notebook/cells';
 
 import {
@@ -70,6 +66,21 @@ class LogStaticNotebook extends StaticNotebook {
     super.onMetadataChanged(model, args);
     this.methods.push('onMetadataChanged');
   }
+
+  protected onCellInserted(index: number, cell: BaseCellWidget): void {
+    super.onCellInserted(index, cell);
+    this.methods.push('onCellInserted');
+  }
+
+  protected onCellMoved(fromIndex: number, toIndex: number): void {
+    super.onCellMoved(fromIndex, toIndex);
+    this.methods.push('onCellMoved');
+  }
+
+  protected onCellRemoved(cell: BaseCellWidget): void {
+    super.onCellRemoved(cell);
+    this.methods.push('onCellRemoved');
+  }
 }
 
 
@@ -99,14 +110,19 @@ class LogNotebook extends Notebook {
     this.methods.push('onUpdateRequest');
   }
 
-  protected onChildAdded(msg: ChildMessage): void {
-    super.onChildAdded(msg);
-    this.methods.push('onChildAdded');
+  protected onCellInserted(index: number, cell: BaseCellWidget): void {
+    super.onCellInserted(index, cell);
+    this.methods.push('onCellInserted');
+  }
+
+  protected onCellMoved(fromIndex: number, toIndex: number): void {
+    super.onCellMoved(fromIndex, toIndex);
+    this.methods.push('onCellMoved');
   }
 
-  protected onChildRemoved(msg: ChildMessage): void {
-    super.onChildRemoved(msg);
-    this.methods.push('onChildRemoved');
+  protected onCellRemoved(cell: BaseCellWidget): void {
+    super.onCellRemoved(cell);
+    this.methods.push('onCellRemoved');
   }
 }
 
@@ -160,6 +176,30 @@ describe('notebook/notebook/widget', () => {
 
     });
 
+    describe('#modelContentChanged', () => {
+
+      it('should be emitted when a cell is added', () => {
+        let widget = new StaticNotebook({ rendermime: defaultRenderMime() });
+        widget.model = new NotebookModel();
+        let called = false;
+        widget.modelContentChanged.connect(() => { called = true; });
+        let cell = widget.model.factory.createCodeCell();
+        widget.model.cells.add(cell);
+        expect(called).to.be(true);
+      });
+
+      it('should be emitted when metadata is set', () => {
+        let widget = new StaticNotebook({ rendermime: defaultRenderMime() });
+        widget.model = new NotebookModel();
+        let called = false;
+        widget.modelContentChanged.connect(() => { called = true; });
+        let cursor = widget.model.getMetadata('foo');
+        cursor.setValue(1);
+        expect(called).to.be(true);
+      });
+
+    });
+
     describe('#model', () => {
 
       it('should get the model for the widget', () => {
@@ -401,6 +441,38 @@ describe('notebook/notebook/widget', () => {
 
     });
 
+    describe('#onCellInserted()', () => {
+
+      it('should be called when a cell is inserted', () => {
+        let widget = createWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        expect(widget.methods).to.contain('onCellInserted');
+      });
+
+    });
+
+    describe('#onCellMoved()', () => {
+
+      it('should be called when a cell is moved', () => {
+        let widget = createWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        widget.model.cells.move(0, 1);
+        expect(widget.methods).to.contain('onCellMoved');
+      });
+
+    });
+
+    describe('#onCellRemoved()', () => {
+
+      it('should be called when a cell is removed', () => {
+        let widget = createWidget();
+        let cell = widget.model.cells.get(0);
+        widget.model.cells.remove(cell);
+        expect(widget.methods).to.contain('onCellRemoved');
+      });
+
+    });
+
     describe('.Renderer', () => {
 
       describe('#createCodeCell()', () => {
@@ -492,6 +564,32 @@ describe('notebook/notebook/widget', () => {
 
     });
 
+    describe('#activeCellChanged', () => {
+
+      it('should be emitted when the active cell changes', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        let called = false;
+        widget.activeCellChanged.connect((sender, args) => {
+          expect(sender).to.be(widget);
+          expect(args).to.be(widget.activeCell);
+          called = true;
+        });
+        widget.activeCellIndex++;
+        expect(called).to.be(true);
+      });
+
+      it('should not be emitted when the active cell does not change', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        let called = false;
+        widget.activeCellChanged.connect(() => { called = true; });
+        widget.activeCellIndex = widget.activeCellIndex;
+        expect(called).to.be(false);
+      });
+
+    });
+
     describe('#mode', () => {
 
       it('should get the interactivity mode of the notebook', () => {
@@ -619,6 +717,27 @@ describe('notebook/notebook/widget', () => {
         widget.activeCellIndex = 1;
       });
 
+      it('should update the active cell if necessary', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        widget.activeCellIndex = 1;
+        expect(widget.activeCell).to.be(widget.childAt(1));
+      });
+
+    });
+
+    describe('#activeCell', () => {
+
+      it('should get the active cell widget', () => {
+        let widget = createActiveWidget();
+        expect(widget.activeCell).to.be(widget.childAt(0));
+      });
+
+      it('should be read-only', () => {
+        let widget = createActiveWidget();
+        expect(() => { widget.activeCell = null; }).to.throwError();
+      });
+
     });
 
     describe('#select()', () => {
@@ -726,6 +845,17 @@ describe('notebook/notebook/widget', () => {
           expect(widget.activeCellIndex).to.be(0);
         });
 
+        it('should preserve "command" mode if in a markdown cell', () => {
+          let cell = widget.model.factory.createMarkdownCell();
+          widget.model.cells.add(cell);
+          let count = widget.childCount();
+          let child = widget.childAt(count - 1) as MarkdownCellWidget;
+          expect(child.rendered).to.be(true);
+          simulate(child.node, 'click');
+          expect(child.rendered).to.be(true);
+          expect(widget.activeCell).to.be(child);
+        });
+
       });
 
       context('dblclick', () => {
@@ -916,18 +1046,24 @@ describe('notebook/notebook/widget', () => {
 
     });
 
-    describe('#onChildAdded()', () => {
+    describe('#onCellInserted()', () => {
 
       it('should post an `update-request', (done) => {
         let widget = createActiveWidget();
         widget.model.fromJSON(DEFAULT_CONTENT);
-        expect(widget.methods).to.contain('onChildAdded');
+        expect(widget.methods).to.contain('onCellInserted');
         requestAnimationFrame(() => {
           expect(widget.methods).to.contain('onUpdateRequest');
           done();
         });
       });
 
+      it('should update the active cell if necessary', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        expect(widget.activeCell).to.be(widget.childAt(0));
+      });
+
       context('`edgeRequested` signal', () => {
 
         it('should activate the previous cell if top is requested', () => {
@@ -951,19 +1087,37 @@ describe('notebook/notebook/widget', () => {
 
     });
 
-    describe('#onChildRemoved()', () => {
+    describe('#onCellMoved()', () => {
+
+      it('should update the active cell index if necessary', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        widget.model.cells.move(1, 0);
+        expect(widget.activeCellIndex).to.be(1);
+      });
+
+    });
+
+    describe('#onCellRemoved()', () => {
 
       it('should post an `update-request', (done) => {
         let widget = createActiveWidget();
         let cell = widget.model.cells.get(0);
         widget.model.cells.remove(cell);
-        expect(widget.methods).to.contain('onChildRemoved');
+        expect(widget.methods).to.contain('onCellRemoved');
         requestAnimationFrame(() => {
           expect(widget.methods).to.contain('onUpdateRequest');
           done();
         });
       });
 
+      it('should update the active cell if necessary', () => {
+        let widget = createActiveWidget();
+        widget.model.fromJSON(DEFAULT_CONTENT);
+        widget.model.cells.removeAt(0);
+        expect(widget.activeCell).to.be(widget.childAt(0));
+      });
+
     });
 
   });

+ 15 - 15
test/src/notebook/notebook/widgetfactory.spec.ts

@@ -82,33 +82,33 @@ describe('notebook/notebook/widgetfactory', () => {
 
       it('should create a new `NotebookPanel` widget', () => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
+        let panel = factory.createNew(context);
         expect(panel).to.be.a(NotebookPanel);
       });
 
       it('should create a clone of the rendermime', () => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
+        let panel = factory.createNew(context);
         expect(panel.rendermime).to.not.be(rendermime);
       });
 
       it('should start a kernel if one is given', () => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context, { name: 'shell' });
+        let panel = factory.createNew(context, { name: 'shell' });
         expect(panel.context.kernel.name).to.be('shell');
       });
 
       it('should start a kernel given the default kernel language', () => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
+        let panel = factory.createNew(context);
         expect(panel.context.kernel.name).to.be('python');
       });
 
@@ -116,17 +116,17 @@ describe('notebook/notebook/widgetfactory', () => {
         let model = new NotebookModel();
         let cursor = model.getMetadata('language_info');
         cursor.setValue({ name: 'shell' });
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
+        let panel = factory.createNew(context);
         expect(panel.context.kernel.name).to.be('shell');
       });
 
       it('should populate the default toolbar items', () => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
+        let panel = factory.createNew(context);
         let items = panel.toolbar.list();
         expect(items).to.contain('save');
         expect(items).to.contain('restart');
@@ -139,10 +139,10 @@ describe('notebook/notebook/widgetfactory', () => {
 
       it('should be a no-op', (done) => {
         let model = new NotebookModel();
-        let context = new MockContext(model);
+        let context = new MockContext<NotebookModel>(model);
         let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(model, context);
-        factory.beforeClose(model, context, panel).then(() => {
+        let panel = factory.createNew(context);
+        factory.beforeClose(panel, context).then(() => {
           done();
         });
       });

+ 15 - 11
tutorial/notebook.md

@@ -113,29 +113,34 @@ We'll walk through two notebook extensions:
 Create a `src/mybutton/plugin.ts` file with the following contents.
 
 ```typescript
+
 import {
-  IWidgetExtension, IDocumentContext, IDocumentModel, DocumentRegistry
-} from '../docregistry';
+  Application
+} from 'phosphide/lib/core/application';
 
 import {
   IDisposable, DisposableDelegate
 } from 'phosphor-disposable';
 
+import {
+  NotebookActions
+} from '../notebook/notebook/actions';
+
 import {
   NotebookPanel
 } from '../notebook/notebook/panel';
 
 import {
-  Application
-} from 'phosphide/lib/core/application';
+  INotebookModel
+} from '../notebook/notebook/model';
 
 import {
   ToolbarButton
 } from '../notebook/notebook/toolbar';
 
 import {
-  NotebookActions
-} from '../notebook/notebook/actions';
+  IWidgetExtension, IDocumentContext, IDocumentModel, DocumentRegistry
+} from '../docregistry';
 
 /**
  * The plugin registration information.
@@ -148,12 +153,11 @@ const widgetExtension = {
 };
 
 export
-class ButtonExtension implements IWidgetExtension<NotebookPanel>{
+class ButtonExtension implements IWidgetExtension<NotebookPanel, INotebookModel> {
   /**
    * Create a new extension object.
    */
-  createNew(nb: NotebookPanel, model: IDocumentModel,
-            context: IDocumentContext): IDisposable {
+  createNew(nb: NotebookPanel, context: IDocumentContext<INotebookModel>): IDisposable {
     let callback = () => {
       NotebookActions.runAll(nb.content, context.kernel);
     };
@@ -164,10 +168,10 @@ class ButtonExtension implements IWidgetExtension<NotebookPanel>{
     });
 
     let i = document.createElement('i');
-    i.classList.add('fa', 'fa-fast-forward')
+    i.classList.add('fa', 'fa-fast-forward');
     button.node.appendChild(i);
 
-    nb.toolbar.add('mybutton', button, 'run')
+    nb.toolbar.add('mybutton', button, 'run');
     return new DisposableDelegate(() => {
       button.dispose();
     });