ソースを参照

Merge pull request #84 from blink1073/document-manager

Document Manager
Jason Grout 8 年 前
コミット
fa2ab6c427

+ 49 - 50
examples/filebrowser/src/index.ts

@@ -6,31 +6,25 @@
 'use strict';
 
 import {
-  ContentsManager, ISessionOptions, NotebookSessionManager
+  ContentsManager, NotebookSessionManager,
+  IKernelSpecIds
 } from 'jupyter-js-services';
 
-import {
-  FileHandler, FileHandlerRegistry, FileCreator, DirectoryCreator
-} from 'jupyter-js-ui/lib/filehandler';
-
 import {
   FileBrowserWidget, FileBrowserModel
 } from 'jupyter-js-ui/lib/filebrowser';
 
 import {
-  showDialog, okButton
-} from 'jupyter-js-ui/lib/dialog';
+  DocumentManager, DocumentWidget
+} from 'jupyter-js-ui/lib/docmanager';
 
 import {
-  CodeMirrorWidget
-} from 'jupyter-js-ui/lib/codemirror/widget';
+  ModelFactory, WidgetFactory
+} from 'jupyter-js-ui/lib/docmanager/default';
 
 import {
-  getConfigOption
-} from 'jupyter-js-utils';
-
-import * as arrays
- from 'phosphor-arrays';
+  showDialog, okButton
+} from 'jupyter-js-ui/lib/dialog';
 
 import {
   DockPanel
@@ -41,46 +35,62 @@ import {
 } from 'phosphor-keymap';
 
 import {
-  Menu, MenuBar, MenuItem
+  Menu, MenuItem
 } from 'phosphor-menus';
 
 import {
   SplitPanel
 } from 'phosphor-splitpanel';
 
-import {
-  Widget
-} from 'phosphor-widget';
-
 import 'jupyter-js-ui/lib/index.css';
 import 'jupyter-js-ui/lib/theme.css';
 
 
 function main(): void {
+  let sessionsManager = new NotebookSessionManager();
+  sessionsManager.getSpecs().then(specs => {
+    createApp(sessionsManager, specs);
+  });
+}
 
-  let baseUrl = getConfigOption('baseUrl');
-  let contentsManager = new ContentsManager(baseUrl);
-  let sessionsManager = new NotebookSessionManager({ baseUrl: baseUrl });
-
-  let fbModel = new FileBrowserModel(contentsManager, sessionsManager);
-  let registry = new FileHandlerRegistry();
-  let fileHandler = new FileHandler(contentsManager);
-
-  registry.addDefaultHandler(fileHandler);
 
-  let fbWidget = new FileBrowserWidget(fbModel, registry);
+function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecIds): void {
+  let contentsManager = new ContentsManager();
+  let widgets: DocumentWidget[] = [];
+  let activeWidget: DocumentWidget;
 
-  let dirCreator = new DirectoryCreator(contentsManager);
-  let fileCreator = new FileCreator(contentsManager);
-  registry.addCreator(
-    'New Directory', dirCreator.createNew.bind(dirCreator));
-  registry.addCreator('New File', fileCreator.createNew.bind(fileCreator));
+  let opener = {
+    open: (widget: DocumentWidget) => {
+      if (widgets.indexOf(widget) === -1) {
+        dock.insertTabAfter(widget);
+        widgets.push(widget);
+      }
+      dock.selectWidget(widget);
+      activeWidget = widget;
+      widget.disposed.connect((w: DocumentWidget) => {
+        let index = widgets.indexOf(w);
+        widgets.splice(index, 1);
+      });
+    }
+  };
 
-  let widgets: CodeMirrorWidget[] = [];
-  registry.opened.connect((r, widget) => {
-    dock.insertTabAfter(widget);
-    widgets.push(widget as CodeMirrorWidget);
+  let fbModel = new FileBrowserModel(contentsManager, sessionsManager);
+  let docManager = new DocumentManager(contentsManager, sessionsManager, specs, opener);
+  let mFactory = new ModelFactory();
+  let wFactory = new WidgetFactory();
+  docManager.registerModelFactory(mFactory, {
+    name: 'default',
+    contentsOptions: { format: 'text', type: 'file' }
+  });
+  docManager.registerWidgetFactory(wFactory, {
+    displayName: 'Editor',
+    modelName: 'default',
+    fileExtensions: ['.*'],
+    defaultFor: ['.*'],
+    preferKernel: false,
+    canStartKernel: true
   });
+  let fbWidget = new FileBrowserWidget(fbModel, docManager, opener);
 
   let panel = new SplitPanel();
   panel.id = 'main';
@@ -91,8 +101,6 @@ function main(): void {
   SplitPanel.setStretch(dock, 1);
   dock.spacing = 8;
 
-  let activeWidget: CodeMirrorWidget;
-
   document.addEventListener('focus', event => {
     for (let i = 0; i < widgets.length; i++) {
       let widget = widgets[i];
@@ -109,29 +117,24 @@ function main(): void {
     selector: '.jp-DirListing',
     handler: () => {
       fbWidget.open();
-      return true;
     }
   }, {
     sequence: ['Ctrl N'], // Add emacs keybinding for select next.
     selector: '.jp-DirListing',
     handler: () => {
       fbWidget.selectNext();
-      return true;
     }
   }, {
     sequence: ['Ctrl P'], // Add emacs keybinding for select previous.
     selector: '.jp-DirListing',
     handler: () => {
       fbWidget.selectPrevious();
-      return true;
     }
   }, {
     sequence: ['Accel S'],
     selector: '.jp-CodeMirrorWidget',
     handler: () => {
-      let path = fileHandler.findPath(activeWidget);
-      fileHandler.save(path);
-      return true;
+      activeWidget.context.save();
     }
   }]);
 
@@ -186,10 +189,6 @@ function main(): void {
       icon: 'fa fa-download',
       handler: () => { fbWidget.download(); }
     }),
-    new MenuItem({
-      text: 'Revert',
-      handler: () => { fbWidget.revert(); }
-    }),
     new MenuItem({
       text: 'Shutdown Kernel',
       icon: 'fa fa-stop-circle-o',

+ 1 - 1
examples/package.json

@@ -4,7 +4,7 @@
   "dependencies": {
     "jupyter-js-ui": "file:..",
     "phosphor-dockpanel": "^0.9.6",
-    "phosphor-keymap": "^0.7.1",
+    "phosphor-keymap": "^0.8.0",
     "phosphor-splitpanel": "^1.0.0-rc.1"
   },
   "scripts": {

+ 2 - 2
package.json

@@ -8,7 +8,7 @@
     "ansi_up": "^1.3.0",
     "backbone": "^1.2.0",
     "codemirror": "^5.12.0",
-    "jupyter-js-services": "^0.9.2",
+    "jupyter-js-services": "^0.10.0",
     "jupyter-js-utils": "^0.4.0",
     "jupyter-js-widgets": "0.0.17",
     "marked": "^0.3.5",
@@ -55,7 +55,7 @@
     "build:src": "tsc --project src && node scripts/copycss.js",
     "build:test": "cd test && tsc --project src && webpack --config webpack.conf.js",
     "build": "npm run build:src && npm run build:test",
-    "docs": "typedoc --mode file --module commonjs --excludeNotExported --target es5 --moduleResolution node --out docs/ src",
+    "docs": "typedoc --mode modules --module commonjs --excludeNotExported --target es5 --moduleResolution node --out docs/ src",
     "prepublish": "npm run build",
     "postinstall": "npm dedupe",
     "test:chrome": "karma start --browsers=Chrome test/karma.conf.js",

+ 6 - 6
src/codemirror/widget.ts

@@ -67,20 +67,20 @@ class CodeMirrorWidget extends Widget {
    */
   protected onAfterAttach(msg: Message): void {
     if (!this.isVisible) {
-      this._dirty = true;
+      this._needsRefresh = true;
       return;
     }
     this._editor.refresh();
-    this._dirty = false;
+    this._needsRefresh = false;
   }
 
   /**
    * A message handler invoked on an `'after-show'` message.
    */
   protected onAfterShow(msg: Message): void {
-    if (this._dirty) {
+    if (this._needsRefresh) {
       this._editor.refresh();
-      this._dirty = false;
+      this._needsRefresh = false;
     }
   }
 
@@ -93,9 +93,9 @@ class CodeMirrorWidget extends Widget {
     } else {
       this._editor.setSize(msg.width, msg.height);
     }
-    this._dirty = false;
+    this._needsRefresh = false;
   }
 
   private _editor: CodeMirror.Editor = null;
-  private _dirty = true;
+  private _needsRefresh = true;
 }

+ 534 - 0
src/docmanager/context.ts

@@ -0,0 +1,534 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import {
+  IKernelId, IKernel, IKernelSpecIds, IContentsManager,
+  INotebookSessionManager, INotebookSession, ISessionId,
+  IContentsOpts, ISessionOptions, IContentsModel
+} from 'jupyter-js-services';
+
+import * as utils
+  from 'jupyter-js-utils';
+
+import {
+  IDisposable
+} from 'phosphor-disposable';
+
+import {
+  ISignal, Signal
+} from 'phosphor-signaling';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+import {
+  IDocumentContext, IDocumentModel, IModelFactoryOptions
+} from './index';
+
+
+/**
+ * An implementation of a document context.
+ */
+class Context implements IDocumentContext {
+  /**
+   * Construct a new document context.
+   */
+  constructor(manager: ContextManager) {
+    this._manager = manager;
+    this._id = utils.uuid();
+  }
+
+  /**
+   * A signal emitted when the kernel changes.
+   */
+  get kernelChanged(): ISignal<IDocumentContext, IKernel> {
+    return Private.kernelChangedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when the path changes.
+   */
+  get pathChanged(): ISignal<IDocumentContext, string> {
+    return Private.pathChangedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when the model is saved or reverted.
+   */
+  get dirtyCleared(): ISignal<IDocumentContext, void> {
+    return Private.dirtyClearedSignal.bind(this);
+  }
+
+  /**
+   * The unique id of the context.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get id(): string {
+    return this._id;
+  }
+
+  /**
+   * The current kernel associated with the document.
+   *
+   * #### Notes
+   * This is a read-only propery.
+   */
+  get kernel(): IKernel {
+    return this._manager.getKernel(this._id);
+  }
+
+  /**
+   * The current path associated with the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get path(): string {
+    return this._manager.getPath(this._id);
+  }
+
+  /**
+   * The current contents model associated with the document
+   *
+   * #### Notes
+   * This is a read-only property.  The model will have an
+   * empty `contents` field.
+   */
+  get contentsModel(): IContentsModel {
+    return this._manager.getContentsModel(this._id);
+  }
+
+  /**
+   * Get the kernel spec information.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get kernelSpecs(): IKernelSpecIds {
+    return this._manager.getKernelSpecs();
+  }
+
+  /**
+   * Get whether the context has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._manager === null;
+  }
+
+  /**
+   * Dispose of the resources held by the context.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._manager = null;
+    this._id = '';
+  }
+
+  /**
+   * Change the current kernel associated with the document.
+   */
+  changeKernel(options: IKernelId): Promise<IKernel> {
+    return this._manager.changeKernel(this._id, options);
+  }
+
+  /**
+   * Save the document contents to disk.
+   */
+  save(): Promise<void> {
+    return this._manager.save(this._id);
+  }
+
+  /**
+   * Save the document to a different path.
+   */
+  saveAs(path: string): Promise<void> {
+    return this._manager.saveAs(this._id, path);
+  }
+
+  /**
+   * Revert the document contents to disk contents.
+   */
+  revert(): Promise<void> {
+    return this._manager.revert(this._id);
+  }
+
+  /**
+   * Get the list of running sessions.
+   */
+  listSessions(): Promise<ISessionId[]> {
+    return this._manager.listSessions();
+  }
+
+  /**
+   * Add a sibling widget to the document manager.
+   *
+   * @param widget - The widget to add to the document manager.
+   *
+   * @returns A disposable used to remove the sibling if desired.
+   *
+   * #### Notes
+   * It is assumed that the widget has the same model and context
+   * as the original widget.
+   */
+  addSibling(widget: Widget): IDisposable {
+    return this._manager.addSibling(this._id, widget);
+  }
+
+  private _id = '';
+  private _manager: ContextManager = null;
+}
+
+
+/**
+ * An object which manages the active contexts.
+ */
+export
+class ContextManager implements IDisposable {
+  /**
+   * Construct a new context manager.
+   */
+  constructor(contentsManager: IContentsManager, sessionManager: INotebookSessionManager,  kernelSpecs: IKernelSpecIds, opener: (id: string, widget: Widget) => IDisposable) {
+    this._contentsManager = contentsManager;
+    this._sessionManager = sessionManager;
+    this._opener = opener;
+    this._kernelspecids = kernelSpecs;
+  }
+
+  /**
+   * Get whether the context manager has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._contentsManager === null;
+  }
+
+  /**
+   * Dispose of the resources held by the document manager.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._contentsManager = null;
+    this._sessionManager = null;
+    this._kernelspecids = null;
+    for (let id in this._contexts) {
+      let contextEx = this._contexts[id];
+      contextEx.context.dispose();
+      contextEx.model.dispose();
+      let session = contextEx.session;
+      if (session) {
+        session.dispose();
+      }
+    }
+    this._contexts = null;
+    this._opener = null;
+  }
+
+  /**
+   * Create a new context.
+   */
+  createNew(path: string, model: IDocumentModel, options: IModelFactoryOptions): string {
+    let context = new Context(this);
+    let id = context.id;
+    this._contexts[id] = {
+      context,
+      path,
+      model,
+      modelName: options.name,
+      opts: options.contentsOptions,
+      contentsModel: null,
+      session: null
+    };
+    return id;
+  }
+
+  /**
+   * Get a context for a given path and model name.
+   */
+  findContext(path: string, modelName: string): string {
+    for (let id in this._contexts) {
+      let contextEx = this._contexts[id];
+      if (contextEx.path === path && contextEx.modelName === modelName) {
+        return id;
+      }
+    }
+  }
+
+  /**
+   * Find a context by path.
+   */
+  getIdsForPath(path: string): string[] {
+    let ids: string[] = [];
+    for (let id in this._contexts) {
+      if (this._contexts[id].path === path) {
+        ids.push(id);
+      }
+    }
+    return ids;
+  }
+
+  /**
+   * Get a context by id.
+   */
+  getContext(id: string): IDocumentContext {
+    return this._contexts[id].context;
+  }
+
+  /**
+   * Get the model associated with a context.
+   */
+  getModel(id: string): IDocumentModel {
+    return this._contexts[id].model;
+  }
+
+  /**
+   * Remove a context.
+   */
+  removeContext(id: string): void {
+    let contextEx = this._contexts[id];
+    contextEx.model.dispose();
+    contextEx.context.dispose();
+    delete this._contexts[id];
+  }
+
+  /**
+   * Get the current kernel associated with a document.
+   */
+  getKernel(id: string): IKernel {
+    let session = this._contexts[id].session;
+    return session ? session.kernel : null;
+  }
+
+  /**
+   * Get the current path associated with a document.
+   */
+  getPath(id: string): string {
+    return this._contexts[id].path;
+  }
+
+  /**
+   * Get the current contents model associated with a document.
+   */
+  getContentsModel(id: string): IContentsModel {
+    return this._contexts[id].contentsModel;
+  }
+
+  /**
+   * Change the current kernel associated with the document.
+   */
+  changeKernel(id: string, options: IKernelId): Promise<IKernel> {
+    let contextEx = this._contexts[id];
+    let session = contextEx.session;
+    if (!session) {
+      let path = contextEx.path;
+      let sOptions = {
+        notebook: { path },
+        kernel: { options }
+      };
+      return this._startSession(id, sOptions);
+    } else {
+      return session.changeKernel(options);
+    }
+  }
+
+  /**
+   * Update the path of an open document.
+   *
+   * @param id - The id of the context.
+   *
+   * @param newPath - The new path.
+   */
+  rename(oldPath: string, newPath: string): void {
+    // Update all of the paths, but only update one session
+    // so there is only one REST API call.
+    let ids = this.getIdsForPath(oldPath);
+    let sessionUpdated = false;
+    for (let id of ids) {
+      let contextEx = this._contexts[id];
+      contextEx.path = newPath;
+      contextEx.context.pathChanged.emit(newPath);
+      if (!sessionUpdated) {
+        let session = contextEx.session;
+        if (session) {
+          session.renameNotebook(newPath);
+          sessionUpdated = true;
+        }
+      }
+    }
+  }
+
+  /**
+   * Get the current kernelspec information.
+   */
+  getKernelSpecs(): IKernelSpecIds {
+    return this._kernelspecids;
+  }
+
+  /**
+   * Save the document contents to disk.
+   */
+  save(id: string): Promise<void> {
+    let contextEx =  this._contexts[id];
+    let opts = utils.copy(contextEx.opts);
+    let path = contextEx.path;
+    let model = contextEx.model;
+    if (model.readOnly) {
+      return Promise.reject(new Error('Read only'));
+    }
+    if (opts.format === 'json') {
+      opts.content = model.toJSON();
+    } else {
+      opts.content = model.toString();
+    }
+    return this._contentsManager.save(path, opts).then(contents => {
+      contextEx.contentsModel = this._copyContentsModel(contents);
+      model.dirty = false;
+    });
+  }
+
+  /**
+   * Save a document to a new file name.
+   *
+   * This results in a new session.
+   */
+  saveAs(id: string, newPath: string): Promise<void> {
+    let contextEx = this._contexts[id];
+    contextEx.path = newPath;
+    contextEx.context.pathChanged.emit(newPath);
+    if (contextEx.session) {
+      let options = {
+        notebook: { path: newPath },
+        kernel: { id: contextEx.session.id }
+      };
+      return this._startSession(id, options).then(() => {
+        return this.save(id);
+      });
+    }
+    return this.save(id);
+  }
+
+  /**
+   * Revert the contents of a path.
+   */
+  revert(id: string): Promise<void> {
+    let contextEx = this._contexts[id];
+    let opts = contextEx.opts;
+    let path = contextEx.path;
+    let model = contextEx.model;
+    return this._contentsManager.get(path, opts).then(contents => {
+      if (contents.format === 'json') {
+        model.fromJSON(contents.content);
+      } else {
+        model.fromString(contents.content);
+      }
+      contextEx.contentsModel = this._copyContentsModel(contents);
+      model.dirty = false;
+    });
+  }
+
+  /**
+   * Get the list of running sessions.
+   */
+  listSessions(): Promise<ISessionId[]> {
+    return this._sessionManager.listRunning();
+  }
+
+  /**
+   * Add a sibling widget to the document manager.
+   */
+  addSibling(id: string, widget: Widget): IDisposable {
+    let opener = this._opener;
+    return opener(id, widget);
+  }
+
+  /**
+   * Start a session and set up its signals.
+   */
+  private _startSession(id: string, options: ISessionOptions): Promise<IKernel> {
+    let contextEx = this._contexts[id];
+    let context = contextEx.context;
+    return this._sessionManager.startNew(options).then(session => {
+      if (contextEx.session) {
+        contextEx.session.dispose();
+      }
+      contextEx.session = session;
+      context.kernelChanged.emit(session.kernel);
+      session.notebookPathChanged.connect((s, path) => {
+        if (path !== contextEx.path) {
+          contextEx.path = path;
+          context.pathChanged.emit(path);
+        }
+      });
+      session.kernelChanged.connect((s, kernel) => {
+        context.kernelChanged.emit(kernel);
+      });
+      return session.kernel;
+    });
+  }
+
+  /**
+   * Copy the contents of a contents model, without the content.
+   */
+  private _copyContentsModel(model: IContentsModel): IContentsModel {
+    return {
+      path: model.path,
+      name: model.name,
+      type: model.type,
+      writable: model.writable,
+      created: model.created,
+      last_modified: model.last_modified,
+      mimetype: model.mimetype,
+      format: model.format
+    };
+  }
+
+  private _contentsManager: IContentsManager = null;
+  private _sessionManager: INotebookSessionManager = null;
+  private _kernelspecids: IKernelSpecIds = null;
+  private _contexts: { [key: string]: Private.IContextEx } = Object.create(null);
+  private _opener: (id: string, widget: Widget) => IDisposable = null;
+}
+
+
+/**
+ * A namespace for private data.
+ */
+namespace Private {
+  /**
+   * An extended interface for data associated with a context.
+   */
+  export
+  interface IContextEx {
+    context: IDocumentContext;
+    model: IDocumentModel;
+    session: INotebookSession;
+    opts: IContentsOpts;
+    path: string;
+    contentsModel: IContentsModel;
+    modelName: string;
+  }
+
+  /**
+   * A signal emitted when the kernel changes.
+   */
+  export
+  const kernelChangedSignal = new Signal<IDocumentContext, IKernel>();
+
+  /**
+   * A signal emitted when the path changes.
+   */
+  export
+  const pathChangedSignal = new Signal<IDocumentContext, string>();
+
+  /**
+   * A signal emitted when the model is saved or reverted.
+   */
+  export
+  const dirtyClearedSignal = new Signal<IDocumentContext, void>();
+}

+ 320 - 0
src/docmanager/default.ts

@@ -0,0 +1,320 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import * as CodeMirror
+  from 'codemirror';
+
+import {
+  IKernelId
+} from 'jupyter-js-services';
+
+import {
+  ISignal, Signal
+} from 'phosphor-signaling';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+import {
+  loadModeByFileName
+} from '../codemirror';
+
+import {
+  CodeMirrorWidget
+} from '../codemirror/widget';
+
+import {
+  IDocumentModel, IWidgetFactory, IDocumentContext
+} from './index';
+
+
+/**
+ * The class name added to a dirty widget.
+ */
+const DIRTY_CLASS = 'jp-mod-dirty';
+
+/**
+ * The class name added to a jupyter code mirror widget.
+ */
+const EDITOR_CLASS = 'jp-CodeMirrorWidget';
+
+
+/**
+ * The default implementation of a document model.
+ */
+export
+class DocumentModel implements IDocumentModel {
+  /**
+   * Construct a new document model.
+   */
+  constructor(languagePreference: string) {
+    this._defaultLang = languagePreference;
+  }
+
+  /**
+   * Get whether the model factory has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * A signal emitted when the document content changes.
+   */
+  get contentChanged(): ISignal<IDocumentModel, string> {
+    return Private.contentChangedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when the document dirty state changes.
+   */
+  get dirtyChanged(): ISignal<IDocumentModel, boolean> {
+    return Private.dirtyChangedSignal.bind(this);
+  }
+
+  /**
+   * The dirty state of the document.
+   */
+  get dirty(): boolean {
+    return this._dirty;
+  }
+  set dirty(value: boolean) {
+    if (value === this._dirty) {
+      return;
+    }
+    this._dirty = value;
+    this.dirtyChanged.emit(value);
+  }
+
+  /**
+   * The read only state of the document.
+   */
+  get readOnly(): boolean {
+    return this._readOnly;
+  }
+  set readOnly(value: boolean) {
+    if (value === this._readOnly) {
+      return;
+    }
+    this._readOnly = value;
+  }
+
+  /**
+   * The default kernel name of the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get defaultKernelName(): string {
+    return '';
+  }
+
+  /**
+   * The default kernel language of the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get defaultKernelLanguage(): string {
+    return this._defaultLang;
+  }
+
+  /**
+   * Dispose of the resources held by the document manager.
+   */
+  dispose(): void {
+    this._isDisposed = true;
+  }
+
+  /**
+   * Serialize the model to a string.
+   */
+  toString(): string {
+    return this._text;
+  }
+
+  /**
+   * Deserialize the model from a string.
+   *
+   * #### Notes
+   * Should emit a [contentChanged] signal.
+   */
+  fromString(value: string): void {
+    if (this._text === value) {
+      return;
+    }
+    this._text = value;
+    this.contentChanged.emit(value);
+    this.dirty = true;
+  }
+
+  /**
+   * Serialize the model to JSON.
+   */
+  toJSON(): any {
+    return JSON.stringify(this._text);
+  }
+
+  /**
+   * Deserialize the model from JSON.
+   *
+   * #### Notes
+   * Should emit a [contentChanged] signal.
+   */
+  fromJSON(value: any): void {
+    this.fromString(JSON.parse(value));
+  }
+
+  private _text = '';
+  private _defaultLang = '';
+  private _dirty = false;
+  private _readOnly = false;
+  private _isDisposed = false;
+}
+
+
+/**
+ * The default implementation of a model factory.
+ */
+export
+class ModelFactory {
+  /**
+   * Get whether the model factory has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Dispose of the resources held by the document manager.
+   */
+  dispose(): void {
+    this._isDisposed = true;
+  }
+
+  /**
+   * Create a new model for a given path.
+   *
+   * @param languagePreference - An optional kernel language preference.
+   *
+   * @returns A new document model.
+   */
+  createNew(languagePreference?: string): IDocumentModel {
+    return new DocumentModel(languagePreference);
+  }
+
+  /**
+   * Get the preferred kernel language given a path.
+   */
+  preferredLanguage(path: string): string {
+    // TODO: use a mapping of extension to language.
+    return '';
+  }
+
+  private _isDisposed = false;
+}
+
+
+/**
+ * A document widget for codemirrors.
+ */
+export
+class EditorWidget extends CodeMirrorWidget {
+  /**
+   * Construct a new editor widget.
+   */
+  constructor(model: IDocumentModel, context: IDocumentContext) {
+    super();
+    this.addClass(EDITOR_CLASS);
+    let editor = this.editor;
+    let doc = editor.getDoc();
+    doc.setValue(model.toString());
+    this.title.text = context.path.split('/').pop();
+    loadModeByFileName(editor, context.path);
+    model.dirtyChanged.connect((m, value) => {
+      if (value) {
+        this.title.className += ` ${DIRTY_CLASS}`;
+      } else {
+        this.title.className = this.title.className.replace(DIRTY_CLASS, '');
+      }
+    });
+    context.pathChanged.connect((c, path) => {
+      loadModeByFileName(editor, path);
+      this.title.text = path.split('/').pop();
+    });
+    model.contentChanged.connect((m, text) => {
+      let old = doc.getValue();
+      if (old !== text) {
+        doc.setValue(text);
+      }
+    });
+    CodeMirror.on(doc, 'change', (instance, change) => {
+      if (change.origin !== 'setValue') {
+        model.fromString(instance.getValue());
+      }
+    });
+  }
+}
+
+
+/**
+ * The default implemetation of a widget factory.
+ */
+export
+class WidgetFactory implements IWidgetFactory<EditorWidget> {
+  /**
+   * Get whether the model factory has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Dispose of the resources held by the document manager.
+   */
+  dispose(): void {
+    this._isDisposed = true;
+  }
+
+  /**
+   * Create a new widget given a document model and a context.
+   */
+  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): EditorWidget {
+    // TODO: if a kernel id or a name other than 'none' or 'default'
+    // was given, start that kernel
+    return new EditorWidget(model, context);
+  }
+
+  /**
+   * Take an action on a widget before closing it.
+   *
+   * @returns A promise that resolves to true if the document should close
+   *   and false otherwise.
+   */
+  beforeClose(model: IDocumentModel, context: IDocumentContext, widget: Widget): Promise<boolean> {
+    // There is nothing specific to do.
+    return Promise.resolve(true);
+  }
+
+  private _isDisposed = false;
+}
+
+
+/**
+ * A private namespace for data.
+ */
+namespace Private {
+  /**
+   * A signal emitted when a document content changes.
+   */
+  export
+  const contentChangedSignal = new Signal<IDocumentModel, string>();
+
+  /**
+   * A signal emitted when a document dirty state changes.
+   */
+  export
+  const dirtyChangedSignal = new Signal<IDocumentModel, boolean>();
+}

+ 948 - 0
src/docmanager/index.ts

@@ -0,0 +1,948 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import {
+  IContentsModel, IKernelId, IContentsOpts, IKernel,
+  IContentsManager, INotebookSessionManager,
+  IKernelSpecIds, ISessionId
+} from 'jupyter-js-services';
+
+import {
+  IDisposable, DisposableDelegate
+} from 'phosphor-disposable';
+
+import {
+  Message
+} from 'phosphor-messaging';
+
+import {
+  PanelLayout
+} from 'phosphor-panel';
+
+import {
+  ISignal
+} from 'phosphor-signaling';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+import {
+  showDialog
+} from '../dialog';
+
+import {
+  IWidgetOpener
+} from '../filebrowser/browser';
+
+import {
+  ContextManager
+} from './context';
+
+
+/**
+ * The class name added to a document container widgets.
+ */
+const DOCUMENT_CLASS = 'jp-DocumentWidget';
+
+
+/**
+ * The interface for a document model.
+ */
+export
+interface IDocumentModel extends IDisposable {
+  /**
+   * A signal emitted when the document content changes.
+   */
+  contentChanged: ISignal<IDocumentModel, any>;
+
+  /**
+   * A signal emitted when the model dirty state changes.
+   */
+  dirtyChanged: ISignal<IDocumentModel, boolean>;
+
+  /**
+   * The dirty state of the model.
+   *
+   * #### Notes
+   * This should be cleared when the document is loaded from
+   * or saved to disk.
+   */
+  dirty: boolean;
+
+  /**
+   * The read-only state of the model.
+   */
+  readOnly: boolean;
+
+  /**
+   * The default kernel name of the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  defaultKernelName: string;
+
+  /**
+   * The default kernel language of the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  defaultKernelLanguage: string;
+
+  /**
+   * Serialize the model to a string.
+   */
+  toString(): string;
+
+  /**
+   * Deserialize the model from a string.
+   *
+   * #### Notes
+   * Should emit a [contentChanged] signal.
+   */
+  fromString(value: string): void;
+
+  /**
+   * Serialize the model to JSON.
+   */
+  toJSON(): any;
+
+  /**
+   * Deserialize the model from JSON.
+   *
+   * #### Notes
+   * Should emit a [contentChanged] signal.
+   */
+  fromJSON(value: any): void;
+}
+
+
+/**
+ * The document context object.
+ */
+export interface IDocumentContext extends IDisposable {
+  /**
+   * The unique id of the context.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  id: string;
+
+  /**
+   * The current kernel associated with the document.
+   *
+   * #### Notes
+   * This is a read-only propery.
+   */
+  kernel: IKernel;
+
+  /**
+   * The current path associated with the document.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  path: string;
+
+  /**
+   * The current contents model associated with the document
+   *
+   * #### Notes
+   * This is a read-only property.  The model will have an
+   * empty `contents` field.
+   */
+  contentsModel: IContentsModel;
+
+  /**
+   * Get the kernel spec information.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  kernelSpecs: IKernelSpecIds;
+
+  /**
+   * A signal emitted when the kernel changes.
+   */
+  kernelChanged: ISignal<IDocumentContext, IKernel>;
+
+  /**
+   * A signal emitted when the path changes.
+   */
+  pathChanged: ISignal<IDocumentContext, string>;
+
+  /**
+   * Change the current kernel associated with the document.
+   */
+  changeKernel(options: IKernelId): Promise<IKernel>;
+
+  /**
+   * Save the document contents to disk.
+   */
+  save(): Promise<void>;
+
+  /**
+   * Save the document to a different path.
+   */
+  saveAs(path: string): Promise<void>;
+
+  /**
+   * Revert the document contents to disk contents.
+   */
+  revert(): Promise<void>;
+
+  /**
+   * Get the list of running sessions.
+   */
+  listSessions(): Promise<ISessionId[]>;
+
+  /**
+   * Add a sibling widget to the document manager.
+   *
+   * @param widget - The widget to add to the document manager.
+   *
+   * @returns A disposable used to remove the sibling if desired.
+   *
+   * #### Notes
+   * It is assumed that the widget has the same model and context
+   * as the original widget.
+   */
+  addSibling(widget: Widget): IDisposable;
+}
+
+
+
+/**
+ * The options used to register a widget factory.
+ */
+export
+interface IWidgetFactoryOptions {
+
+  /**
+   * The file extensions the widget can view.
+   *
+   * #### Notes
+   * Use `'.*'` to denote all file extensions
+   * or give the actual extension (e.g. `'.txt'`).
+   */
+  fileExtensions: string[];
+
+  /**
+   * The name of the widget to display in dialogs.
+   */
+  displayName: string;
+
+  /**
+   * The registered name of the model type used to create the widgets.
+   */
+  modelName: string;
+
+  /**
+   * The file extensions for which the factory should be the default.
+   * #### Notes
+   * Use `'.*'` to denote all file extensions
+   * or give the actual extension (e.g. `'.txt'`).
+   */
+  defaultFor?: string[];
+
+  /**
+   * Whether the widgets prefer having a kernel started.
+   */
+  preferKernel?: boolean;
+
+  /**
+   * Whether the widgets can start a kernel when opened.
+   */
+  canStartKernel?: boolean;
+}
+
+
+/**
+ * The interface for a widget factory.
+ */
+export
+interface IWidgetFactory<T extends Widget> extends IDisposable {
+  /**
+   * Create a new widget.
+   */
+  createNew(model: IDocumentModel, context: IDocumentContext, kernel?: IKernelId): T;
+
+  /**
+   * Take an action on a widget before closing it.
+   *
+   * @returns A promise that resolves to true if the document should close
+   *   and false otherwise.
+   */
+  beforeClose(model: IDocumentModel, context: IDocumentContext, widget: Widget): Promise<boolean>;
+}
+
+
+/**
+ * The options used to register a model factory.
+ */
+export
+interface IModelFactoryOptions {
+  /**
+   * The name of the model factory.
+   */
+  name: string;
+
+  /**
+   * The contents options used to fetch/save files.
+   */
+  contentsOptions: IContentsOpts;
+}
+
+
+/**
+ * The interface for a model factory.
+ */
+export
+interface IModelFactory extends IDisposable {
+  /**
+   * Create a new model for a given path.
+   *
+   * @param languagePreference - An optional kernel language preference.
+   *
+   * @returns A new document model.
+   */
+  createNew(languagePreference?: string): IDocumentModel;
+
+  /**
+   * Get the preferred kernel language given a path.
+   */
+  preferredLanguage(path: string): string;
+}
+
+
+/**
+ * A kernel preference for a given file path and widget.
+ */
+export
+interface IKernelPreference {
+  /**
+   * The preferred kernel language.
+   */
+  language: string;
+
+  /**
+   * Whether to prefer having a kernel started when opening.
+   */
+  preferKernel: boolean;
+
+  /**
+   * Whether a kernel when can be started when opening.
+   */
+  canStartKernel: boolean;
+}
+
+
+/**
+ * The document manager.
+ *
+ * #### Notes
+ * The document manager is used to register model and widget creators,
+ * and the file browser uses the document manager to create widgets. The
+ * document manager maintains a context for each path and model type that is
+ * open, and a list of widgets for each context. The document manager is in
+ * control of the proper closing and disposal of the widgets and contexts.
+ */
+export
+class DocumentManager implements IDisposable {
+  /**
+   * Construct a new document manager.
+   */
+  constructor(contentsManager: IContentsManager, sessionManager: INotebookSessionManager, kernelSpecs: IKernelSpecIds, opener: IWidgetOpener) {
+    this._contentsManager = contentsManager;
+    this._sessionManager = sessionManager;
+    this._contextManager = new ContextManager(contentsManager, sessionManager, kernelSpecs, (id: string, widget: Widget) => {
+      let parent = this._createWidget('', id);
+      parent.setContent(widget);
+      opener.open(parent);
+      return new DisposableDelegate(() => {
+        parent.close();
+      });
+    });
+  }
+
+  /**
+   * Get whether the document manager has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._contentsManager === null;
+  }
+
+  /**
+   * Dispose of the resources held by the document manager.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    for (let modelName in this._modelFactories) {
+      this._modelFactories[modelName].factory.dispose();
+    }
+    this._modelFactories = null;
+    for (let widgetName in this._widgetFactories) {
+      this._widgetFactories[widgetName].factory.dispose();
+    }
+    this._widgetFactories = null;
+    for (let id in this._widgets) {
+      for (let widget of this._widgets[id]) {
+        widget.dispose();
+      }
+    }
+    this._widgets = null;
+    this._contentsManager = null;
+    this._sessionManager = null;
+    this._contextManager.dispose();
+    this._contextManager = null;
+  }
+
+  /**
+   * Register a widget factory with the document manager.
+   *
+   * @param factory - The factory instance.
+   *
+   * @param options - The options used to register the factory.
+   *
+   * @returns A disposable used to unregister the factory.
+   *
+   * #### Notes
+   * If a factory with the given `displayName` is already registered,
+   * an error will be thrown.
+   * If `'.*'` is given as a default extension, the factory will be registered
+   * as the global default.
+   * 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.
+   */
+  registerWidgetFactory(factory: IWidgetFactory<Widget>, options: IWidgetFactoryOptions): IDisposable {
+    let name = options.displayName;
+    let exOpt = options as Private.IWidgetFactoryEx;
+    exOpt.factory = factory;
+    if (this._widgetFactories[name]) {
+      throw new Error(`Duplicate registered factory ${name}`);
+    }
+    this._widgetFactories[name] = exOpt;
+    if (options.defaultFor) {
+      for (let option of options.defaultFor) {
+        if (option === '.*') {
+          this._defaultWidgetFactory = name;
+        } else if (option in options.fileExtensions) {
+          this._defaultWidgetFactories[option] = name;
+        }
+      }
+    }
+    return new DisposableDelegate(() => {
+      delete this._widgetFactories[name];
+      if (this._defaultWidgetFactory === name) {
+        this._defaultWidgetFactory = '';
+      }
+      for (let opt of Object.keys(this._defaultWidgetFactories)) {
+        let n = this._defaultWidgetFactories[opt];
+        if (n === name) {
+          delete this._defaultWidgetFactories[opt];
+        }
+      }
+    });
+  }
+
+  /**
+   * Register a model factory.
+   *
+   * @param factory - The factory instance.
+   *
+   * @param options - The options used to register the factory.
+   *
+   * @returns A disposable used to unregister the factory.
+   *
+   * #### Notes
+   * If a factory with the given `name` is already registered, an error
+   * will be thrown.
+   */
+  registerModelFactory(factory: IModelFactory, options: IModelFactoryOptions): IDisposable {
+    let exOpt = options as Private.IModelFactoryEx;
+    let name = options.name;
+    exOpt.factory = factory;
+    if (this._modelFactories[name]) {
+      throw new Error(`Duplicate registered factory ${name}`);
+    }
+    this._modelFactories[name] = exOpt;
+    return new DisposableDelegate(() => {
+      delete this._modelFactories[name];
+    });
+  }
+
+  /**
+   * Get the list of registered widget factory display names.
+   *
+   * @param path - An optional file path to filter the results.
+   *
+   * #### Notes
+   * The first item in the list is considered the default.
+   */
+  listWidgetFactories(path?: string): string[] {
+    path = path || '';
+    let ext = '.' + path.split('.').pop();
+    let factories: string[] = [];
+    let options: Private.IWidgetFactoryEx;
+    let name = '';
+    // If an extension was given, filter by extension.
+    // Make sure the modelFactory is registered.
+    if (ext.length > 1) {
+      if (ext in this._defaultWidgetFactories) {
+        name = this._defaultWidgetFactories[ext];
+        options = this._widgetFactories[name];
+        if (options.modelName in this._modelFactories) {
+          factories.push(name);
+        }
+      }
+    }
+    // Add the rest of the valid widgetFactories that can open the path.
+    for (name in this._widgetFactories) {
+      if (factories.indexOf(name) !== -1) {
+        continue;
+      }
+      options = this._widgetFactories[name];
+      if (!(options.modelName in this._modelFactories)) {
+        continue;
+      }
+      let exts = options.fileExtensions;
+      if ((ext in exts) || ('.*' in exts)) {
+        factories.push(name);
+      }
+    }
+    // Add the default widget if it was not already added.
+    name = this._defaultWidgetFactory;
+    if (name && factories.indexOf(name) === -1) {
+      options = this._widgetFactories[name];
+      if (options.modelName in this._modelFactories) {
+        factories.push(name);
+      }
+    }
+    return factories;
+  }
+
+  /**
+   * Get the kernel preference.
+   */
+  getKernelPreference(path: string, widgetName: string): IKernelPreference {
+    let widgetFactoryEx = this._getWidgetFactoryEx(widgetName);
+    let modelFactoryEx = this._getModelFactoryEx(widgetName);
+    let language = modelFactoryEx.factory.preferredLanguage(path);
+    return {
+      language,
+      preferKernel: widgetFactoryEx.preferKernel,
+      canStartKernel: widgetFactoryEx.canStartKernel
+    };
+  }
+
+  /**
+   * Open a file and return the widget used to display the contents.
+   *
+   * @param path - The file path to open.
+   *
+   * @param widgetName - The name of the widget factory to use.
+   *
+   * @param kernel - An optional kernel name/id to override the default.
+   */
+  open(path: string, widgetName='default', kernel?: IKernelId): Widget {
+    if (widgetName === 'default') {
+      widgetName = this.listWidgetFactories(path)[0];
+    }
+    let mFactoryEx = this._getModelFactoryEx(widgetName);
+    if (!mFactoryEx) {
+      return;
+    }
+    let widget: DocumentWidget;
+    // Use an existing context if available.
+    let id = this._contextManager.findContext(path, mFactoryEx.name);
+    if (id) {
+      widget = this._createWidget(widgetName, id);
+      this._populateWidget(widget, kernel);
+      return widget;
+    }
+    let lang = mFactoryEx.factory.preferredLanguage(path);
+    let model = mFactoryEx.factory.createNew(lang);
+    id = this._contextManager.createNew(path, model, mFactoryEx);
+    widget = this._createWidget(widgetName, id);
+    // Load the contents from disk.
+    this._contextManager.revert(id).then(() => {
+      this._populateWidget(widget, kernel);
+    });
+    return widget;
+  }
+
+  /**
+   * Create a new file of the given name.
+   *
+   * @param path - The file path to use.
+   *
+   * @param widgetName - The name of the widget factory to use.
+   *
+   * @param kernel - An optional kernel name/id to override the default.
+   */
+  createNew(path: string, widgetName='default', kernel?: IKernelId): Widget {
+    if (widgetName === 'default') {
+      widgetName = this.listWidgetFactories(path)[0];
+    }
+    let mFactoryEx = this._getModelFactoryEx(widgetName);
+    if (!mFactoryEx) {
+      return;
+    }
+    let lang = mFactoryEx.factory.preferredLanguage(path);
+    let model = mFactoryEx.factory.createNew(lang);
+    let id = this._contextManager.createNew(path, model, mFactoryEx);
+    let widget = this._createWidget(widgetName, id);
+    // Save the contents to disk to get a valid contentsModel for the
+    // context.
+    this._contextManager.save(id).then(() => {
+      this._populateWidget(widget, kernel);
+    });
+    return widget;
+  }
+
+  /**
+   * Handle the renaming of an open document.
+   *
+   * @param oldPath - The previous path.
+   *
+   * @param newPath - The new path.
+   */
+  handleRename(oldPath: string, newPath: string): void {
+    this._contextManager.rename(oldPath, newPath);
+  }
+
+  /**
+   * Handle a file deletion.
+   */
+  handleDelete(path: string): void {
+    // TODO: Leave all of the widgets open and flag them as orphaned?
+  }
+
+  /**
+   * See if a widget already exists for the given path and widget name.
+   *
+   * #### Notes
+   * This can be used to use an existing widget instead of opening
+   * a new widget.
+   */
+  findWidget(path: string, widgetName='default'): Widget {
+    let ids = this._contextManager.getIdsForPath(path);
+    if (widgetName === 'default') {
+      widgetName = this._defaultWidgetFactory;
+    }
+    for (let id of ids) {
+      for (let widget of this._widgets[id]) {
+        if (widget.name === widgetName) {
+          return widget;
+        }
+      }
+    }
+  }
+
+  /**
+   * Clone a widget.
+   *
+   * #### Notes
+   * This will create a new widget with the same model and context
+   * as this widget.
+   */
+  clone(widget: DocumentWidget): DocumentWidget {
+    let parent = this._createWidget(widget.name, widget.context.id);
+    this._populateWidget(parent);
+    return parent;
+  }
+
+  /**
+   * Close the widgets associated with a given path.
+   */
+  closeFile(path: string): void {
+    let ids = this._contextManager.getIdsForPath(path);
+    for (let id of ids) {
+      let widgets: DocumentWidget[] = this._widgets[id] || [];
+      for (let w of widgets) {
+        w.close();
+      }
+    }
+  }
+
+  /**
+   * Close all of the open documents.
+   */
+  closeAll(): void {
+    for (let id in this._widgets) {
+      for (let w of this._widgets[id]) {
+        w.close();
+      }
+    }
+  }
+
+  /**
+   * Create a container widget and handle its lifecycle.
+   */
+  private _createWidget(name: string, id: string): DocumentWidget {
+    let factory = this._widgetFactories[name].factory;
+    let widget = new DocumentWidget(name, id, this._contextManager, factory, this._widgets);
+    if (!(id in this._widgets)) {
+      this._widgets[id] = [];
+    }
+    this._widgets[id].push(widget);
+    return widget;
+  }
+
+  /**
+   * Create a content widget and add it to the container widget.
+   */
+  private _populateWidget(parent: DocumentWidget, kernel?: IKernelId): void {
+    let factory = this._widgetFactories[parent.name].factory;
+    let id = parent.context.id;
+    let model = this._contextManager.getModel(id);
+    let context = this._contextManager.getContext(id);
+    let child = factory.createNew(model, context, kernel);
+    parent.setContent(child);
+  }
+
+  /**
+   * Get the appropriate widget factory by name.
+   */
+  private _getWidgetFactoryEx(widgetName: string): Private.IWidgetFactoryEx {
+    let options: Private.IWidgetFactoryEx;
+    if (widgetName === 'default') {
+      options = this._widgetFactories[this._defaultWidgetFactory];
+    } else {
+      options = this._widgetFactories[widgetName];
+    }
+    return options;
+  }
+
+  /**
+   * Get the appropriate model factory given a widget factory.
+   */
+  private _getModelFactoryEx(widgetName: string): Private.IModelFactoryEx {
+    let wFactoryEx = this._getWidgetFactoryEx(widgetName);
+    if (!wFactoryEx) {
+      return;
+    }
+    return this._modelFactories[wFactoryEx.modelName];
+  }
+
+  private _modelFactories: { [key: string]: Private.IModelFactoryEx } = Object.create(null);
+  private _widgetFactories: { [key: string]: Private.IWidgetFactoryEx } = Object.create(null);
+  private _defaultWidgetFactory = '';
+  private _defaultWidgetFactories: { [key: string]: string } = Object.create(null);
+  private _widgets: { [key: string]: DocumentWidget[] } = Object.create(null);
+  private _contentsManager: IContentsManager = null;
+  private _sessionManager: INotebookSessionManager = null;
+  private _contextManager: ContextManager = null;
+}
+
+
+/**
+ * A container widget for documents.
+ */
+export
+class DocumentWidget extends Widget {
+  /**
+   * Construct a new document widget.
+   */
+  constructor(name: string, id: string, manager: ContextManager, factory: IWidgetFactory<Widget>, widgets: { [key: string]: DocumentWidget[] }) {
+    super();
+    this.addClass(DOCUMENT_CLASS);
+    this.layout = new PanelLayout();
+    this._name = name;
+    this._id = id;
+    this._manager = manager;
+    this._widgets = widgets;
+    this.title.closable = true;
+  }
+
+  /**
+   * Get the name of the widget.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get name(): string {
+    return this._name;
+  }
+
+  /**
+   * The context for the widget.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get context(): IDocumentContext {
+    return this._manager.getContext(this._id);
+  }
+
+  /**
+   * Bring up a dialog to select a kernel.
+   */
+  selectKernel(): Promise<IKernel> {
+    // TODO: the dialog should take kernel information only,
+    // and return kernel information.  We then change the
+    // kernel in the context.
+    return void 0;
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    // Remove the widget from the widget registry.
+    let id = this._id;
+    let index = this._widgets[id].indexOf(this);
+    this._widgets[id].splice(index, 1);
+    // Dispose of the context if this is the last widget using it.
+    if (!this._widgets[id].length) {
+      this._manager.removeContext(id);
+    }
+    this._manager = null;
+    this._factory = null;
+    this._widgets = null;
+    super.dispose();
+  }
+
+  /**
+   * Set the child the widget.
+   *
+   * #### Notes
+   * This function is not intended to be called by user code.
+   */
+  setContent(child: Widget): void {
+    let layout = this.layout as PanelLayout;
+    if (layout.childAt(0)) {
+      throw new Error('Content already set');
+    }
+    this.title.text = child.title.text;
+    this.title.icon = child.title.icon;
+    this.title.className = child.title.className;
+    // Mirror this title based on the child.
+    child.title.changed.connect(() => {
+      this.title.text = child.title.text;
+      this.title.icon = child.title.icon;
+      this.title.className = child.title.className;
+    });
+    // Add the child widget to the layout.
+    (this.layout as PanelLayout).addChild(child);
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onCloseRequest(msg: Message): void {
+    let model = this._manager.getModel(this._id);
+    let layout = this.layout as PanelLayout;
+    let child = layout.childAt(0);
+    // Handle dirty state.
+    this._maybeClose(model.dirty).then(result => {
+      if (result) {
+        // Let the widget factory handle closing.
+        return this._factory.beforeClose(model, this.context, child);
+      }
+      return result;
+    }).then(result => {
+      if (result) {
+        // Perform close tasks.
+        return this._actuallyClose();
+      }
+      return result;
+    }).then(result => {
+      if (result) {
+        // Dispose of document widgets when they are closed.
+        this.dispose();
+      }
+    }).catch(() => {
+      this.dispose();
+    });
+  }
+
+  /**
+   * Ask the user whether to close an unsaved file.
+   */
+  private _maybeClose(dirty: boolean): Promise<boolean> {
+    // Bail if the model is not dirty or other widgets are using the model.
+    let widgets = this._widgets[this._id];
+    if (!dirty || widgets.length > 1) {
+      return Promise.resolve(true);
+    }
+    return showDialog({
+      title: 'Close without saving?',
+      body: `File "${this.title.text}" has unsaved changes, close without saving?`,
+      host: this.node
+    }).then(value => {
+      if (value && value.text === 'OK') {
+        return true;
+      }
+      return false;
+    });
+  }
+
+  /**
+   * Perform closing tasks for the widget.
+   */
+  private _actuallyClose(): Promise<boolean> {
+    // Check for a dangling kernel.
+    let widgets = this._widgets[this._id];
+    let kernelId = this.context.kernel ? this.context.kernel.id : '';
+    if (!kernelId || widgets.length > 1) {
+      return Promise.resolve(true);
+    }
+    for (let id in this._widgets) {
+      for (let widget of this._widgets[id]) {
+        let kId = widget.context.kernel || widget.context.kernel.id;
+        if (widget !== this && kId === kernelId) {
+          return Promise.resolve(true);
+        }
+      }
+    }
+    return showDialog({
+      title: 'Shut down kernel?',
+      body: `Shut down ${this.context.kernel.name}?`,
+      host: this.node
+    }).then(value => {
+      if (value && value.text === 'OK') {
+        return this.context.kernel.shutdown();
+      }
+    }).then(() => {
+      return true;
+    });
+  }
+
+  private _manager: ContextManager = null;
+  private _factory: IWidgetFactory<Widget> = null;
+  private _id = '';
+  private _name = '';
+  private _widgets: { [key: string]: DocumentWidget[] } = null;
+}
+
+
+/**
+ * A private namespace for DocumentManager data.
+ */
+namespace Private {
+  /**
+   * An extended interface for a model factory and its options.
+   */
+  export
+  interface IModelFactoryEx extends IModelFactoryOptions {
+    factory: IModelFactory;
+  }
+
+  /**
+   * An extended interface for a widget factory and its options.
+   */
+  export
+  interface IWidgetFactoryEx extends IWidgetFactoryOptions {
+    factory: IWidgetFactory<Widget>;
+  }
+}

+ 56 - 0
src/docmanager/theme.css

@@ -0,0 +1,56 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) 2014-2016, Jupyter Development Team.
+|
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+.jp-CodeMirrorWidget {
+  height: 100%;
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror {
+  line-height: 1.21429em;
+  /* Changed from 1em to our global default */
+  font-size: 14px;
+  height: 100%;
+  /* Changed to auto to autogrow */
+  background: none;
+  /* Changed from white to allow our bg to show through */
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror-scroll {
+  /*  The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/
+  /*  We have found that if it is visible, vertical scrollbars appear with font size changes.*/
+  overflow-y: hidden;
+  overflow-x: auto;
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror-lines {
+  /* In CM2, this used to be 0.4em, but in CM3 it went to 4px. We need the em value because */
+  /* we have set a different line-height and want this to scale with that. */
+  padding: 0.4em;
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror-linenumber {
+  padding: 0 8px 0 4px;
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror-gutters {
+  border-bottom-left-radius: 2px;
+  border-top-left-radius: 2px;
+}
+
+
+.jp-CodeMirrorWidget > .CodeMirror pre {
+  /* In CM3 this went to 4px from 0 in CM2. We need the 0 value because of how we size */
+  /* .CodeMirror-lines */
+  padding: 0;
+  border: 0;
+  border-radius: 0;
+}
+
+

+ 28 - 55
src/filebrowser/browser.ts

@@ -22,8 +22,8 @@ import {
 } from 'phosphor-widget';
 
 import {
-  FileHandlerRegistry
-} from '../filehandler';
+  DocumentManager
+} from '../docmanager';
 
 import {
   FileButtons
@@ -67,6 +67,15 @@ const LISTING_CLASS = 'jp-FileBrowser-listing';
 const REFRESH_DURATION = 30000;
 
 
+/**
+ * An interface for a widget opener.
+ */
+export
+interface IWidgetOpener {
+  open(widget: Widget): void;
+}
+
+
 /**
  * A widget which hosts a file browser.
  *
@@ -81,18 +90,23 @@ class FileBrowserWidget extends Widget {
    *
    * @param model - The file browser view model.
    */
-  constructor(model: FileBrowserModel, registry: FileHandlerRegistry) {
+  constructor(model: FileBrowserModel, manager: DocumentManager, opener: IWidgetOpener) {
     super();
     this.addClass(FILE_BROWSER_CLASS);
     this._model = model;
     this._model.refreshed.connect(this._handleRefresh, this);
     this._crumbs = new BreadCrumbs(model);
-    this._buttons = new FileButtons(model, registry);
-    this._listing = new DirListing(model, registry);
-    this._registry = registry;
+    this._buttons = new FileButtons(model, manager, opener);
+    this._listing = new DirListing(model, manager, opener);
+    this._manager = manager;
+    this._opener = opener;
 
     model.fileChanged.connect((fbModel, args) => {
-      registry.rename(args.oldValue, args.newValue);
+      if (args.newValue) {
+        manager.handleRename(args.oldValue, args.newValue);
+      } else {
+        manager.handleDelete(args.oldValue);
+      }
     });
 
     this._crumbs.addClass(CRUMBS_CLASS);
@@ -126,7 +140,8 @@ class FileBrowserWidget extends Widget {
     this._crumbs = null;
     this._buttons = null;
     this._listing = null;
-    this._registry = null;
+    this._manager = null;
+    this._opener = null;
     super.dispose();
   }
 
@@ -155,52 +170,9 @@ class FileBrowserWidget extends Widget {
           showErrorMessage(this, 'Open directory', error)
         );
       } else {
-        this._registry.open(item.path);
-      }
-    }
-  }
-
-  /**
-   * Revert the currently selected item(s).
-   */
-  revert(): void {
-    let items = this._model.sortedItems;
-    for (let item of items) {
-      if (!this._model.isSelected(item.name)) {
-        continue;
-      }
-      if (item.type !== 'directory') {
-        this._registry.revert(item.path);
-      }
-    }
-  }
-
-  /**
-   * Save the currently selected item(s).
-   */
-  save(): void {
-    let items = this._model.sortedItems;
-    for (let item of items) {
-      if (!this._model.isSelected(item.name)) {
-        continue;
-      }
-      if (item.type !== 'directory') {
-        this._registry.save(item.path);
-      }
-    }
-  }
-
-  /**
-   * Close the currently selected item(s).
-   */
-  close(): void {
-    let items = this._model.sortedItems;
-    for (let item of items) {
-      if (!this._model.isSelected(item.name)) {
-        continue;
-      }
-      if (item.type !== 'directory') {
-        this._registry.close(item.path);
+        let path = item.path;
+        let widget = this._manager.findWidget(path) || this._manager.open(item.path);
+        this._opener.open(widget);
       }
     }
   }
@@ -320,5 +292,6 @@ class FileBrowserWidget extends Widget {
   private _buttons: FileButtons = null;
   private _listing: DirListing = null;
   private _timeoutId = -1;
-  private _registry: FileHandlerRegistry = null;
+  private _manager: DocumentManager = null;
+  private _opener: IWidgetOpener = null;
 }

+ 118 - 46
src/filebrowser/buttons.ts

@@ -19,13 +19,17 @@ import {
 } from '../dialog';
 
 import {
-  FileHandlerRegistry
-} from '../filehandler';
+  DocumentManager
+} from '../docmanager';
 
 import {
   FileBrowserModel
 } from './model';
 
+import {
+  IWidgetOpener
+} from './browser';
+
 import * as utils
   from './utils';
 
@@ -50,11 +54,6 @@ const CONTENT_CLASS = 'jp-FileButtons-buttonContent';
  */
 const ICON_CLASS = 'jp-FileButtons-buttonIcon';
 
-/**
- * The class name added to a dropdown icon.
- */
-const DROPDOWN_CLASS = 'jp-FileButtons-dropdownIcon';
-
 /**
  * The class name added to the create button.
  */
@@ -75,6 +74,11 @@ const REFRESH_CLASS = 'jp-id-refresh';
  */
 const ACTIVE_CLASS = 'jp-mod-active';
 
+/**
+ * The class name added to a dropdown icon.
+ */
+const DROPDOWN_CLASS = 'jp-FileButtons-dropdownIcon';
+
 
 /**
  * A widget which hosts the file browser buttons.
@@ -86,7 +90,7 @@ class FileButtons extends Widget {
    *
    * @param model - The file browser view model.
    */
-  constructor(model: FileBrowserModel, registry: FileHandlerRegistry) {
+  constructor(model: FileBrowserModel, manager: DocumentManager, opener: IWidgetOpener) {
     super();
     this.addClass(FILE_BUTTONS_CLASS);
     this._model = model;
@@ -101,7 +105,8 @@ class FileButtons extends Widget {
     node.appendChild(this._buttons.upload);
     node.appendChild(this._buttons.refresh);
 
-    this._registry = registry;
+    this._manager = manager;
+    this._opener = opener;
   }
 
   /**
@@ -111,7 +116,8 @@ class FileButtons extends Widget {
     this._model = null;
     this._buttons = null;
     this._input = null;
-    this._registry = null;
+    this._manager = null;
+    this._opener = null;
     super.dispose();
   }
 
@@ -126,10 +132,19 @@ class FileButtons extends Widget {
   }
 
   /**
-   * Get the file handler registry used by the widget.
+   * Get the document manager used by the widget.
    */
-  get registry(): FileHandlerRegistry {
-    return this._registry;
+  get manager(): DocumentManager {
+    return this._manager;
+  }
+
+  /**
+   * Open a file by path.
+   */
+  open(path: string): void {
+    let widget = this._manager.open(path);
+    let opener = this._opener;
+    opener.open(widget);
   }
 
   /**
@@ -171,11 +186,14 @@ class FileButtons extends Widget {
     dropdown.popup(rect.left, rect.bottom, false, true);
   };
 
+
   /**
    * The 'click' handler for the upload button.
    */
   private _onUploadButtonClicked = (event: MouseEvent) => {
-    if (event.button !== 0) return;
+    if (event.button !== 0) {
+      return;
+    }
     this._input.click();
   };
 
@@ -183,7 +201,9 @@ class FileButtons extends Widget {
    * The 'click' handler for the refresh button.
    */
   private _onRefreshButtonClicked = (event: MouseEvent) => {
-    if (event.button !== 0) return;
+    if (event.button !== 0) {
+      return;
+    }
     this._model.refresh().catch(error => {
       utils.showErrorMessage(this, 'Server Connection Error', error);
     });
@@ -200,7 +220,8 @@ class FileButtons extends Widget {
   private _model: FileBrowserModel;
   private _buttons = Private.createButtons();
   private _input = Private.createUploadInput();
-  private _registry: FileHandlerRegistry = null
+  private _manager: DocumentManager = null;
+  private _opener: IWidgetOpener = null;
 }
 
 
@@ -282,43 +303,90 @@ namespace Private {
   }
 
   /**
-   * Create a new dropdown menu for the create new button.
+   * Create a new source file.
    */
   export
-  function createDropdownMenu(widget: FileButtons): Menu {
-    let registry = widget.registry;
-    let creators = registry.listCreators();
-    creators = creators.sort((a, b) => a.localeCompare(b));
-    let items: MenuItem[] = [];
-    for (var text of creators) {
-      items.push(createItem(text, widget));
-    }
-    return new Menu(items);
+  function createNewFile(widget: FileButtons): void {
+    createFile(widget, 'file').then(contents => {
+      if (contents === void 0) {
+        return;
+      }
+      widget.model.refresh().then(() => widget.open(contents.name));
+    }).catch(error => {
+      utils.showErrorMessage(widget, 'New File Error', error);
+    });
+  }
+
+  /**
+   * Create a new folder.
+   */
+  export
+  function createNewFolder(widget: FileButtons): void {
+    createFile(widget, 'directory').then(contents => {
+      if (contents === void 0) {
+        return;
+      }
+      widget.model.refresh();
+    }).catch(error => {
+      utils.showErrorMessage(widget, 'New Folder Error', error);
+    });
   }
 
   /**
-   * Create a menu item in the dropdown menu.
+   * Create a new notebook.
    */
-  function createItem(text: string, widget: FileButtons): MenuItem {
-    let registry = widget.registry;
-    let model = widget.model;
-    let host = widget.parent.node;
-    return new MenuItem({
-      text,
-      handler: () => {
-        registry.createNew(text, model.path, host).then(contents => {
-          if (contents === void 0) {
-            return;
-          }
-          if (contents.type !== 'directory') {
-            registry.open(contents.path);
-          }
-          model.refresh();
-        });
+  export
+  function createNewNotebook(widget: FileButtons, spec: IKernelSpecId): void {
+    createFile(widget, 'notebook').then(contents => {
+      let started = widget.model.startSession(contents.path, spec.name);
+      return started.then(() => contents);
+    }).then(contents => {
+      if (contents === void 0) {
+        return;
       }
+      widget.model.refresh().then(() => widget.open(contents.name));
+    }).catch(error => {
+      utils.showErrorMessage(widget, 'New Notebook Error', error);
     });
   }
 
+  /**
+   * Create a new file, prompting the user for a name.
+   */
+  function createFile(widget: FileButtons, type: string): Promise<IContentsModel> {
+    return widget.model.newUntitled(type);
+  }
+
+  /**
+   * Create a new dropdown menu for the create new button.
+   */
+  export
+  function createDropdownMenu(widget: FileButtons): Menu {
+    let items = [
+      new MenuItem({
+        text: 'Text File',
+        handler: () => { createNewFile(widget); }
+      }),
+      new MenuItem({
+        text: 'Folder',
+        handler: () => { createNewFolder(widget); }
+      }),
+      new MenuItem({
+        type: MenuItem.Separator
+      })
+    ];
+    // TODO the kernels below are suffixed with "Notebook" as a
+    // temporary measure until we can update the Menu widget to
+    // show text in a separator for a "Notebooks" group.
+    let extra = widget.model.kernelSpecs.map(spec => {
+      return new MenuItem({
+        text: `${spec.spec.display_name} Notebook`,
+        handler: () => { createNewNotebook(widget, spec); }
+      });
+    });
+    return new Menu(items.concat(extra));
+  }
+
   /**
    * Upload an array of files to the server.
    */
@@ -338,7 +406,9 @@ namespace Private {
   function uploadFile(widget: FileButtons, file: File): Promise<any> {
     return widget.model.upload(file).catch(error => {
       let exists = error.message.indexOf('already exists') !== -1;
-      if (exists) return uploadFileOverride(widget, file);
+      if (exists) {
+        return uploadFileOverride(widget, file);
+      }
       throw error;
     });
   }
@@ -349,11 +419,13 @@ namespace Private {
   function uploadFileOverride(widget: FileButtons, file: File): Promise<any> {
     let options = {
       title: 'Overwrite File?',
-      host: this.parent.node,
+      host: widget.parent.node,
       body: `"${file.name}" already exists, overwrite?`
     };
     return showDialog(options).then(button => {
-      if (button.text !== 'Ok') return;
+      if (button.text !== 'Ok') {
+        return;
+      }
       return widget.model.upload(file, true);
     });
   }

+ 0 - 5
src/filebrowser/index.css

@@ -34,11 +34,6 @@
 }
 
 
-.jp-FileButtons-dropdownIcon {
-  margin-left: -0.5em;
-}
-
-
 .jp-DirListing {
   flex: 1 1 auto;
   display: flex;

+ 18 - 9
src/filebrowser/listing.ts

@@ -29,13 +29,17 @@ import {
 } from '../dialog';
 
 import {
-  FileHandlerRegistry
-} from '../filehandler';
+  DocumentManager
+} from '../docmanager';
 
 import {
   FileBrowserModel
 } from './model';
 
+import {
+  IWidgetOpener
+} from './browser';
+
 import * as utils
   from './utils';
 
@@ -292,7 +296,7 @@ class DirListing extends Widget {
    *
    * @param model - The file browser view model.
    */
-  constructor(model: FileBrowserModel, registry: FileHandlerRegistry) {
+  constructor(model: FileBrowserModel, manager: DocumentManager, opener: IWidgetOpener) {
     super();
     this.addClass(DIR_LISTING_CLASS);
     this._model = model;
@@ -300,7 +304,8 @@ class DirListing extends Widget {
     this._model.selectionChanged.connect(this._onSelectionChanged, this);
     this._editNode = document.createElement('input');
     this._editNode.className = EDITOR_CLASS;
-    this._registry = registry;
+    this._manager = manager;
+    this._opener = opener;
   }
 
   /**
@@ -312,7 +317,8 @@ class DirListing extends Widget {
     this._editNode = null;
     this._drag = null;
     this._dragData = null;
-    this._registry = null;
+    this._manager = null;
+    this._opener = null;
     super.dispose();
   }
 
@@ -879,7 +885,9 @@ class DirListing extends Widget {
         showErrorMessage(this, 'Open directory', error)
       );
     } else {
-      this._registry.open(item.path);
+      let path = item.path;
+      let widget = this._manager.findWidget(path) || this._manager.open(item.path);
+      this._opener.open(widget);
     }
   }
 
@@ -1028,13 +1036,13 @@ class DirListing extends Widget {
     this._drag.mimeData.setData(utils.CONTENTS_MIME, selectedNames);
     if (item && item.type !== 'directory') {
       this._drag.mimeData.setData(FACTORY_MIME, () => {
-        this._registry.open(item.path);
+        let path = item.path;
+        return this._manager.findWidget(path) || this._manager.open(item.path);
       });
     }
 
     // Start the drag and remove the mousemove listener.
     this._drag.start(clientX, clientY).then(action => {
-      console.log('action', action);
       this._drag = null;
       clearTimeout(this._selectTimer);
     });
@@ -1251,7 +1259,8 @@ class DirListing extends Widget {
   private _prevPath = '';
   private _clipboard: string[] = [];
   private _softSelection = '';
-  private _registry: FileHandlerRegistry = null;
+  private _manager: DocumentManager = null;
+  private _opener: IWidgetOpener = null;
 }
 
 

+ 0 - 198
src/filehandler/creator.ts

@@ -1,198 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import {
-  IContentsModel, IContentsManager
-} from 'jupyter-js-services';
-
-import {
-  showDialog
-} from '../dialog';
-
-
-/**
- * A class that creates files for a file registry.
- */
-export
-class FileCreator {
-  /**
-   * Create the dialog for the file creator.
-   */
-  static createDialog(): HTMLElement {
-    let node = document.createElement('div');
-    let input = document.createElement('input');
-    node.appendChild(input);
-    return node;
-  }
-
-  /**
-   * Construct a new file creator.
-   */
-  constructor(manager: IContentsManager, displayName = 'File') {
-    this._manager = manager;
-    this._displayName = displayName;
-    let constructor = this.constructor as typeof FileCreator;
-    this._body = constructor.createDialog();
-  }
-
-  /**
-   * Create a new file object in the given directory.
-   */
-  createNew(path: string, host?: HTMLElement): Promise<IContentsModel> {
-    this.host = host || this._host;
-    return this.createUntitled(path).then(contents => {
-      return this.doRename(contents);
-    }).catch(error => {
-      return this.showErrorMessage(error);
-    });
-  }
-
-  /**
-   * Get the contents manager used by the creator.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  protected get manager(): IContentsManager {
-    return this._manager;
-  }
-
-  /**
-   * Get the host node for file creation dialogs.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  protected get host(): HTMLElement {
-    return this._host;
-  }
-  protected set host(value: HTMLElement) {
-    this._host = value;
-  }
-
-  /**
-   * The input node with the file name.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  protected get fileNode(): HTMLInputElement {
-    return this.body.getElementsByTagName('input')[0];
-  }
-
-  /**
-   * The dialog body.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  protected get body(): HTMLElement {
-    return this._body;
-  }
-
-  /**
-   * The dialog display name.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  protected get displayName(): string {
-    return this._displayName;
-  }
-
-  /**
-   * Rename a file or directory.
-   */
-  protected doRename(contents: IContentsModel): Promise<IContentsModel> {
-    let edit = this.fileNode;
-    edit.value = contents.name;
-    let dname = contents.path.slice(0, -contents.name.length);
-    return showDialog({
-      title: `Create a New ${this._displayName}`,
-      body: this.body,
-      host: this._host,
-      okText: 'CREATE'
-    }).then(value => {
-      if (value && value.text === 'CREATE') {
-        if (edit.value === contents.name) {
-          return contents;
-        }
-        return this.manager.rename(contents.path, `${dname}/${edit.value}`);
-      } else {
-        return this.manager.delete(contents.path).then(() => void 0);
-      }
-    }).catch(error => {
-      if (error.statusText === 'Conflict') {
-        return this.handleExisting(edit.value, contents);
-      }
-      return this.showErrorMessage(error);
-    });
-  }
-
-  /**
-   * Show an error message.
-   */
-  protected showErrorMessage(error: Error): Promise<IContentsModel> {
-    return showDialog({
-      title: `${this.displayName} Creation Error`,
-      body: error.message,
-      host: this.host,
-      okText: 'DISMISS'
-    }).then(() => { return void 0; });
-  }
-
-  /**
-   * Handle an existing file name.
-   */
-  protected handleExisting(name: string, contents: IContentsModel): Promise<IContentsModel> {
-    return showDialog({
-      title: `${this.displayName} already exists`,
-      body: `${this.displayName} "${name}" already exists, try again?`,
-      host: this.host
-    }).then(value => {
-      if (value && value.text === 'OK') {
-        return this.doRename(contents);
-      } else {
-        return this.manager.delete(contents.path).then(() => void 0);
-      }
-    });
-  }
-
-  /**
-   * Create a new untitled file on the current path.
-   */
-  protected createUntitled(path: string): Promise<IContentsModel> {
-    return this.manager.newUntitled(path, {
-      type: 'file', ext: '.txt'
-    });
-  }
-
-  private _manager: IContentsManager = null;
-  private _host: HTMLElement = document.body;
-  private _displayName = '';
-  private _body: HTMLElement = null;
-}
-
-
-
-/**
- * A file creator which creates directories.
- */
-export
-class DirectoryCreator extends FileCreator {
-  /**
-   * Construct a new directory creator.
-   */
-  constructor(manager: IContentsManager, displayName = 'Directory') {
-    super(manager, displayName);
-  }
-
-  /**
-   * Create a new untitled directory on the given path.
-   */
-  protected createUntitled(path: string): Promise<IContentsModel> {
-    return this.manager.newUntitled(path, { type: 'directory' });
-  }
-
-}

+ 0 - 73
src/filehandler/default.ts

@@ -1,73 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import * as CodeMirror
-  from 'codemirror';
-
-import {
-  IContentsModel, IContentsManager, IContentsOpts
-} from 'jupyter-js-services';
-
-import {
-  Message
-} from 'phosphor-messaging';
-
-import {
-  loadModeByFileName
-} from '../codemirror';
-
-import {
-  CodeMirrorWidget
-} from '../codemirror/widget';
-
-import {
-  showDialog
-} from '../dialog';
-
-import {
-  AbstractFileHandler
-} from './handler';
-
-/**
- * The class name added to a jupyter code mirror widget.
- */
-const EDITOR_CLASS = 'jp-CodeMirrorWidget';
-
-
-/**
- * An implementation of a file handler.
- */
-export
-class FileHandler extends AbstractFileHandler<CodeMirrorWidget> {
-  /**
-   * Get the options used to save the widget content.
-   */
-  protected getSaveOptions(widget: CodeMirrorWidget, path: string): Promise<IContentsOpts> {
-    let name = path.split('/').pop();
-    let content = widget.editor.getDoc().getValue();
-    return Promise.resolve({ path, content, name,
-                             type: 'file', format: 'text' });
-  }
-
-  /**
-   * Create the widget from a path.
-   */
-  protected createWidget(path: string): CodeMirrorWidget {
-    let widget = new CodeMirrorWidget();
-    widget.addClass(EDITOR_CLASS);
-    CodeMirror.on(widget.editor.getDoc(), 'change', () => {
-      this.setDirty(path, true);
-    });
-    return widget;
-  }
-
-  /**
-   * Populate a widget from an `IContentsModel`.
-   */
-  protected populateWidget(widget: CodeMirrorWidget, model: IContentsModel): Promise<IContentsModel> {
-    widget.editor.getDoc().setValue(model.content);
-    loadModeByFileName(widget.editor, model.name);
-    return Promise.resolve(model);
-  }
-}

+ 0 - 391
src/filehandler/handler.ts

@@ -1,391 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import {
-  IContentsModel, IContentsManager, IContentsOpts
-} from 'jupyter-js-services';
-
-import {
-  IMessageFilter, IMessageHandler, Message, installMessageFilter
-} from 'phosphor-messaging';
-
-import {
-  Property
-} from 'phosphor-properties';
-
-import {
-  ISignal, Signal
-} from 'phosphor-signaling';
-
-import {
-  Widget
-} from 'phosphor-widget';
-
-import {
-  showDialog
-} from '../dialog';
-
-
-/**
- * The class name added to a dirty documents.
- */
-const DIRTY_CLASS = 'jp-mod-dirty';
-
-
-/**
- * An implementation of a file handler.
- */
-export
-abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
-  /**
-   * Construct a new source file handler.
-   *
-   * @param manager - The contents manager used to save/load files.
-   */
-  constructor(manager: IContentsManager) {
-    this._manager = manager;
-  }
-
-  /**
-   * A signal emitted when a file opens.
-   */
-  get opened(): ISignal<AbstractFileHandler<T>, T> {
-    return Private.openedSignal.bind(this);
-  }
-
-  /**
-   * A signal emitted when a file finishes opening.
-   */
-  get finished(): ISignal<AbstractFileHandler<T>, T> {
-    return Private.finishedSignal.bind(this);
-  }
-
-  /**
-   * Get the list of file extensions explicitly supported by the handler.
-   */
-  get fileExtensions(): string[] {
-    return [];
-  }
-
-  /**
-   * Find a widget given a path.
-   */
-  findWidget(path: string): T {
-    for (let w of this._widgets) {
-      let p = this._getPath(w);
-      if (p === path) {
-        return w;
-      }
-    }
-  }
-
-  /**
-   * Find a path given a widget.
-   */
-  findPath(widget: T): string {
-    return Private.pathProperty.get(widget);
-  }
-
-  /**
-   * Open a file by path and return a widget.
-   */
-  open(path: string): T {
-    let widget = this.findWidget(path);
-    if (!widget) {
-      widget = this.createWidget(path);
-      widget.title.closable = true;
-      widget.title.text = this.getTitleText(path);
-      this._setPath(widget, path);
-      this._widgets.push(widget);
-      installMessageFilter(widget, this);
-    }
-
-    // Fetch the contents and populate the widget asynchronously.
-    let opts = this.getFetchOptions(path);
-    this.manager.get(path, opts).then(contents => {
-      return this.populateWidget(widget, contents);
-    }).then(contents => {
-      this.setDirty(path, false);
-      this.finished.emit(widget);
-    });
-    this.opened.emit(widget);
-    return widget;
-  }
-
-  /**
-   * Rename a file.
-   *
-   * #### Notes
-   * The file action itself must take place elsewhere.
-   */
-  rename(oldPath: string, newPath?: string): boolean {
-    let widget = this.findWidget(oldPath);
-    if (widget === void 0) {
-      return false;
-    }
-    if (newPath === void 0) {
-      this.setDirty(oldPath, false);
-      widget.close();
-      return true;
-    }
-    Private.pathProperty.set(widget, newPath);
-    let parts = newPath.split('/');
-    widget.title.text = this.getTitleText(newPath);
-    return true;
-  }
-
-  /**
-   * Save contents.
-   *
-   * @param path - The path of the file to save.
-   *
-   * returns A promise that resolves to the contents of the path.
-   *
-   * #### Notes
-   * This clears the dirty state of the file after a successful save.
-   */
-  save(path: string): Promise<IContentsModel> {
-    let widget = this.findWidget(path);
-    if (!widget) {
-      return Promise.resolve(void 0);
-    }
-    let model = this._getPath(widget);
-    return this.getSaveOptions(widget, model).then(opts => {
-      return this.manager.save(path, opts);
-    }).then(contents => {
-      this.setDirty(path, false);
-      return contents;
-    });
-  }
-
-  /**
-   * Revert contents.
-   *
-   * @param path - The path of the file to revert.
-   *
-   * returns A promise that resolves to the new contents of the path.
-   *
-   * #### Notes
-   * This clears the dirty state of the file after a successful revert.
-   */
-  revert(path: string): Promise<IContentsModel> {
-    let widget = this.findWidget(path);
-    if (!widget) {
-      return Promise.resolve(void 0);
-    }
-    let opts = this.getFetchOptions(path);
-    return this.manager.get(path, opts).then(contents => {
-      return this.populateWidget(widget, contents);
-    }).then(contents => {
-      this.setDirty(path, false);
-      return contents;
-    });
-  }
-
-  /**
-   * Close a file.
-   *
-   * @param path - The path of the file to close.
-   *
-   * returns A boolean indicating whether the file was closed.
-   */
-  close(path: string): Promise<boolean> {
-    let widget = this.findWidget(path);
-    if (!widget) {
-      return Promise.resolve(false);
-    }
-    if (this.isDirty(path)) {
-      return this._maybeClose(widget);
-    }
-    return this._close(widget);
-  }
-
-  /**
-   * Close all files.
-   */
-  closeAll(): Promise<void> {
-    let promises: Promise<boolean>[] = [];
-    for (let w of this._widgets) {
-      let path = this.findPath(w);
-      promises.push(this.close(path));
-    }
-    return Promise.all(promises).then(() => { return void 0; });
-  }
-
-  /**
-   * Get whether a file is dirty.
-   */
-  isDirty(path: string): boolean {
-    let widget = this.findWidget(path);
-    if (widget) {
-      return Private.dirtyProperty.get(widget);
-    }
-  }
-
-  /**
-   * Set the dirty state of a file.
-   */
-  setDirty(path: string, value: boolean): void {
-    let widget = this.findWidget(path);
-    if (widget) Private.dirtyProperty.set(widget, value);
-  }
-
-  /**
-   * Filter messages on the widget.
-   */
-  filterMessage(handler: IMessageHandler, msg: Message): boolean {
-    let widget = handler as T;
-    if (msg.type === 'close-request') {
-      let path = this.findPath(widget);
-      if (path) {
-        this.close(path);
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Get the contents manager used by the handler.
-   */
-  protected get manager(): IContentsManager {
-    return this._manager;
-  }
-
-  /**
-   * Get options used to fetch the model contents from disk.
-   */
-  protected getFetchOptions(path: string): IContentsOpts {
-    return { type: 'file', format: 'text' };
-  }
-
-  /**
-   * Get the options used to save the widget content.
-   */
-  protected abstract getSaveOptions(widget: T, path: string): Promise<IContentsOpts>;
-
-  /**
-   * Create the widget from a path.
-   */
-  protected abstract createWidget(path: string): T;
-
-  /**
-   * Populate a widget from an `IContentsModel`.
-   *
-   * #### Notes
-   * Subclasses are free to use any or none of the information in
-   * the model.  It is up to subclasses to handle setting dirty state when
-   * the widget contents change.  See [[AbstractFileHandler.dirtyProperty]].
-   */
-  protected abstract populateWidget(widget: T, model: IContentsModel): Promise<IContentsModel>;
-
-  /**
-   * Set the appropriate title text based on a path.
-   */
-  protected getTitleText(path: string): string {
-    return path.split('/').pop();
-  }
-
-  /**
-   * Perform an action before closing the widget.
-   *
-   * #### Notes
-   * The default implementation is a no-op.
-   */
-  protected beforeClose(widget: T): Promise<void> {
-    return Promise.resolve(void 0);
-  }
-
-  /**
-   * Get the path for a given widget.
-   */
-  private _getPath(widget: T): string {
-    return Private.pathProperty.get(widget);
-  }
-
-  /**
-   * Set the path for a widget.
-   */
-  private _setPath(widget: T, path: string) {
-    Private.pathProperty.set(widget, path);
-  }
-
-  /**
-   * Ask the user whether to close an unsaved file.
-   */
-  private _maybeClose(widget: T): Promise<boolean> {
-    let host = widget.isAttached ? widget.node : document.body;
-    return showDialog({
-      title: 'Close without saving?',
-      body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
-      host
-    }).then(value => {
-      if (value && value.text === 'OK') {
-        this._close(widget);
-        return true;
-      }
-      return false;
-    });
-  }
-
-  /**
-   * Actually close the file.
-   */
-  private _close(widget: T): Promise<boolean> {
-    return this.beforeClose(widget).then(() => {
-      let model = Private.pathProperty.get(widget);
-      let index = this._widgets.indexOf(widget);
-      this._widgets.splice(index, 1);
-      Private.pathProperty.set(widget, void 0);
-      widget.close();
-      return true;
-    });
-  }
-
-  private _manager: IContentsManager = null;
-  private _widgets: T[] = [];
-  private _cb: (widget: T) => void  = null;
-}
-
-
-/**
- * A private namespace for AbstractFileHandler data.
- */
-namespace Private {
-  /**
-   * A signal emitted when a path is opened.
-   */
-  export
-  const openedSignal = new Signal<AbstractFileHandler<Widget>, Widget>();
-
-  /**
-   * A signal emitted when a model is populated.
-   */
-  export
-  const finishedSignal = new Signal<AbstractFileHandler<Widget>, Widget>();
-
-  /**
-   * An attached property with the widget path.
-   */
-  export
-  const pathProperty = new Property<Widget, string>({
-    name: 'path'
-  });
-
-  /**
-   * An attached property with the widget dirty state.
-   */
-  export
-  const dirtyProperty = new Property<Widget, boolean>({
-    name: 'dirty',
-    value: false,
-    changed: (widget: Widget, oldValue: boolean, newValue: boolean) => {
-      if (newValue) {
-        widget.title.className += ` ${DIRTY_CLASS}`;
-      } else {
-        widget.title.className = widget.title.className.replace(DIRTY_CLASS, '');
-      }
-    }
-  });
-}

+ 0 - 8
src/filehandler/index.ts

@@ -1,8 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-export * from './handler';
-export * from './creator';
-export * from './default';
-export * from './registry';

+ 0 - 257
src/filehandler/registry.ts

@@ -1,257 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import {
-  IContentsModel
-} from 'jupyter-js-services';
-
-import {
-  ISignal, Signal
-} from 'phosphor-signaling';
-
-import {
-  Widget
-} from 'phosphor-widget';
-
-import {
-  AbstractFileHandler
-} from './handler';
-
-
-/**
- * A registry of file handlers.
- */
-export
-class FileHandlerRegistry {
-  /**
-   * A signal emitted when a file is opened.
-   */
-  get opened(): ISignal<FileHandlerRegistry, Widget> {
-    return Private.openedSignal.bind(this);
-  }
-
-  /**
-   * A signal emitted when a file has finished opening.
-   */
-  get finished(): ISignal<FileHandlerRegistry, Widget> {
-    return Private.finishedSignal.bind(this);
-  }
-
-  /**
-   * A signal emitted when a file is created.
-   */
-  get created(): ISignal<FileHandlerRegistry, IContentsModel> {
-    return Private.createdSignal.bind(this);
-  }
-
-  /**
-   * Register a file handler.
-   */
-  addHandler(handler: AbstractFileHandler<Widget>): void {
-    this._handlers.push(handler);
-    handler.finished.connect((handler, widget) => {
-      this.finished.emit(widget);
-    });
-  }
-
-  /**
-   * Register a default file handler.
-   */
-  addDefaultHandler(handler: AbstractFileHandler<Widget>): void {
-    if (this._default) {
-      throw Error('Default handler already registered');
-    }
-    this.addHandler(handler);
-    this._default = handler;
-  }
-
-  /**
-   * Add a file creator.
-   */
-  addCreator(name: string, handler: (path: string) => Promise<IContentsModel>): void {
-    this._creators[name] = handler;
-  }
-
-  /**
-   * Get a list of file creators.
-   */
-  listCreators(): string[] {
-    return Object.keys(this._creators);
-  }
-
-  /**
-   * Create a new file.
-   */
-  createNew(name: string, path: string, host?: HTMLElement): Promise<IContentsModel> {
-    let creator = this._creators[name];
-    if (creator) {
-      return creator(path, host).then(model => {
-        this.created.emit(model);
-        return model;
-      });
-    }
-    return Promise.reject(new Error(`No handler named ${name}`));
-  }
-
-  /**
-   * Open a file by path.
-   */
-  open(path: string): Widget {
-    let handler = this.findHandler(path);
-    if (handler === void 0) {
-      return;
-    }
-    let widget = handler.open(path);
-    this.opened.emit(widget);
-    return widget;
-  }
-
-  /**
-   * Rename a file.
-   */
-  rename(oldPath: string, newPath?: string): boolean {
-    for (let h of this._handlers) {
-      if (h.findWidget(oldPath)) {
-        return h.rename(oldPath, newPath);
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Save a file.
-   */
-  save(path: string): Promise<IContentsModel> {
-    for (let h of this._handlers) {
-      let w = h.findWidget(path);
-      if (w !== void 0) {
-        return h.save(path);
-      }
-    }
-    let msg = `No open widget for path '${path}'`;
-    return Promise.reject(new Error(msg));
-  }
-
-  /**
-   * Revert a file.
-   */
-  revert(path: string): Promise<IContentsModel> {
-    for (let h of this._handlers) {
-      let w = h.findWidget(path);
-      if (w !== void 0) {
-        return h.revert(path);
-      }
-    }
-    let msg = `No open widget for path '${path}'`;
-    return Promise.reject(new Error(msg));
-  }
-
-  /**
-   * Close a file.
-   */
-  close(path: string): Promise<boolean> {
-    for (let h of this._handlers) {
-      let w = h.findWidget(path);
-      if (w !== void 0) {
-        return h.close(path);
-      }
-    }
-    let msg = `No open widget for path '${path}'`;
-    return Promise.reject(new Error(msg));
-  }
-
-  /**
-   * Close all files.
-   */
-  closeAll(): Promise<void> {
-    let promises: Promise<void>[] = [];
-    for (let h of this._handlers) {
-      promises.push(h.closeAll());
-    }
-    return Promise.all(promises).then(() => { return void 0; });
-  }
-
-  /**
-   * Find the path for a given widget.
-   */
-  findPath(widget: Widget): string {
-    for (let h of this._handlers) {
-      let path = h.findPath(widget);
-      if (path) {
-        return path;
-      }
-    }
-  }
-
-  /**
-   * Find the widget for a given file.
-   */
-  findWidget(path: string): Widget {
-    for (let h of this._handlers) {
-      let w = h.findWidget(path);
-      if (w !== void 0) {
-        return w;
-      }
-    }
-  }
-
-  /**
-   * Find the appropriate handler given a path.
-   */
-  protected findHandler(path: string): AbstractFileHandler<Widget> {
-    if (this._handlers.length === 0) {
-      return;
-    }
-    let ext = '.' + path.split('.').pop();
-    let handlers: AbstractFileHandler<Widget>[] = [];
-    // Look for matching file extensions.
-    for (let h of this._handlers) {
-      if (h.fileExtensions.indexOf(ext) !== -1) handlers.push(h);
-    }
-    // If there was only one match, use it.
-    if (handlers.length === 1) {
-      return handlers[0];
-    }
-
-    // If there were no matches, use default handler.
-    if (handlers.length === 0) {
-      if (this._default) {
-        return this._default;
-      }
-      return;
-    }
-
-    // There are more than one possible handlers.
-    // TODO: Ask the user to choose one.
-    return handlers[0];
-  }
-
-  private _handlers: AbstractFileHandler<Widget>[] = [];
-  private _default: AbstractFileHandler<Widget> = null;
-  private _creators: { [key: string]: (path: string, host?: HTMLElement) => Promise<IContentsModel> } = Object.create(null);
-}
-
-
-/**
- * A private namespace for FileHandlerRegistry data.
- */
-namespace Private {
-  /**
-   * A signal emitted when a file is opened.
-   */
-  export
-  const openedSignal = new Signal<FileHandlerRegistry, Widget>();
-
-  /**
-   * A signal emitted when a file is finished opening.
-   */
-  export
-  const finishedSignal = new Signal<FileHandlerRegistry, Widget>();
-
-  /**
-   * A signal emitted when a file is created.
-   */
-  export
-  const createdSignal = new Signal<FileHandlerRegistry, IContentsModel>();
-}

+ 0 - 17
src/filehandler/tsconfig.json

@@ -1,17 +0,0 @@
-{
-  "compilerOptions": {
-    "declaration": true,
-    "noImplicitAny": true,
-    "noEmitOnError": true,
-    "module": "commonjs",
-    "moduleResolution": "node",
-    "target": "ES5",
-    "outDir": "../lib"
-  },
-  "files": [
-    "../typings/codemirror/codemirror.d.ts",
-    "../typings/require/require.d.ts",
-    "../typings/es6-promise.d.ts",
-    "index.ts"
-  ]
-}

+ 0 - 48
src/filehandler/widget.ts

@@ -1,48 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import {
-  CodeMirrorWidget
-} from '../codemirror/widget';
-
-import {
-  Message
-} from 'phosphor-messaging';
-
-
-/**
- * The class name added to a jupyter code mirror widget.
- */
-const EDITOR_CLASS = 'jp-CodeMirrorWidget';
-
-
-/**
- * A Jupyter-specific code mirror widget.
- */
-export
-class JupyterCodeMirrorWidget extends CodeMirrorWidget {
-  /**
-   * Construct a new jupyter code mirror widget.
-   */
-  constructor() {
-    super();
-    this.addClass(EDITOR_CLASS);
-  }
-
-  /**
-   * A message handler invoked on an `'after-attach'` message.
-   */
-  protected onAfterAttach(msg: Message): void {
-    super.onAfterAttach(msg);
-    this.editor.focus();
-  }
-
-  /**
-   * A message handler invoked on an `'after-show'` message.
-   */
-  protected onAfterShow(msg: Message): void {
-    super.onAfterShow(msg);
-    this.editor.focus();
-  }
-}

+ 3 - 0
src/theme.css

@@ -3,6 +3,9 @@
 |
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
+@import './docmanager/theme.css';
 @import './dialog/theme.css';
 @import './filebrowser/theme.css';
 @import './terminal/theme.css';
+
+

+ 0 - 229
test/src/filehandler/creator.spec.ts

@@ -1,229 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import expect = require('expect.js');
-
-import {
-  MockContentsManager
-} from 'jupyter-js-services/lib/mockcontents';
-
-import {
-  IContentsModel, IContentsManager, ContentsManager
-} from 'jupyter-js-services';
-
-import {
-  FileCreator, DirectoryCreator
-} from '../../../lib/filehandler/creator';
-
-import {
-  acceptDialog, dismissDialog, waitForDialog
-} from '../utils';
-
-
-class MyFileCreator extends FileCreator {
-
-  methods: string[] = [];
-
-  getFileNode(): HTMLInputElement {
-    return this.fileNode;
-  }
-
-  doRename(contents: IContentsModel): Promise<IContentsModel> {
-    this.methods.push('doRename');
-    return super.doRename(contents);
-  }
-
-  showErrorMessage(error: Error): Promise<IContentsModel> {
-    this.methods.push('showErrorMessage');
-    return super.showErrorMessage(error);
-  }
-
-  handleExisting(name: string, contents: IContentsModel): Promise<IContentsModel> {
-    this.methods.push('handleExisting');
-    return super.handleExisting(name, contents);
-  }
-
-  createUntitled(path: string): Promise<IContentsModel> {
-    this.methods.push('createUntitled');
-    return super.createUntitled(name);
-  }
-}
-
-
-class MyDirectoryCreator extends DirectoryCreator {
-
-  methods: string[] = [];
-
-  createUntitled(path: string): Promise<IContentsModel> {
-    this.methods.push('createUntitled');
-    return super.createUntitled(name);
-  }
-}
-
-
-describe('jupyter-ui', () => {
-
-  describe('FileCreator', () => {
-
-    describe('#constructor()', () => {
-
-      it('should accept a contents manager and a display name', () => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager, 'File');
-        expect(creator instanceof FileCreator).to.be(true);
-      });
-
-      it('should provide a default display name', () => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        expect(creator instanceof FileCreator).to.be(true);
-      });
-
-    });
-
-    describe('#createNew', () => {
-
-      it('should delete the temp file if the dialog is dismissed', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        creator.createNew('foo').then(contents => {
-          expect(manager.methods.indexOf('delete')).to.not.be(-1);
-          expect(contents).to.be(void 0);
-          done();
-        });
-        dismissDialog();
-      });
-
-      it('should create the untitled file if dialog is accepted', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        creator.createNew('foo').then(contents => {
-          expect(contents.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-        acceptDialog();
-      });
-
-      it('should create the untitled file if dialog is accepted', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        creator.createNew('foo').then(contents => {
-          expect(contents.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-        acceptDialog();
-      });
-
-      it('should support an accepted rename', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyFileCreator(manager);
-        creator.createNew('foo').then(contents => {
-          expect(contents.name).to.be('bar.txt');
-          expect(manager.methods.indexOf('rename')).to.not.be(-1);
-          done();
-        });
-        waitForDialog().then(() => {
-          let node = creator.getFileNode();
-          node.value = 'bar.txt';
-          acceptDialog();
-        });
-      });
-
-    });
-
-    describe('#showErrorMessage', () => {
-
-      it('should pop up a dialog', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyFileCreator(manager);
-        creator.showErrorMessage(new Error('text')).then(() => {
-          done();
-        });
-        acceptDialog();
-      });
-
-    });
-
-    describe('#handleExisting()', () => {
-
-      it('should trigger a rename if accepted', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyFileCreator(manager);
-        manager.newUntitled('file').then(contents => {
-          return creator.handleExisting('foo', contents);
-        }).then(() => {
-          expect(creator.methods.indexOf('doRename')).to.not.be(-1);
-          done();
-        });
-        acceptDialog().then(() => {
-          acceptDialog();
-        });
-      });
-
-      it('should delete the file if dismissed', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyFileCreator(manager);
-        manager.newUntitled('file').then(contents => {
-          return creator.handleExisting('foo', contents);
-        }).then(() => {
-          expect(manager.methods.indexOf('delete')).to.not.be(-1);
-          done();
-        });
-        dismissDialog();
-      });
-
-    });
-
-    describe('#createUntitled()', () => {
-
-      it('should create a new untitled file on the given path', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyFileCreator(manager);
-        creator.createUntitled('foo/').then(contents => {
-          expect(contents.type).to.be('file');
-          expect(contents.name.indexOf('.txt')).to.not.be(-1);
-          expect(manager.methods.indexOf('newUntitled')).to.not.be(-1);
-          done();
-        });
-      });
-
-    });
-
-  });
-
-  describe('DirectoryCreator', () => {
-
-    describe('#constructor', () => {
-
-      it('should accept a contents manager and a display name', () => {
-        let manager = new MockContentsManager();
-        let creator = new DirectoryCreator(manager, 'Directory');
-        expect(creator instanceof DirectoryCreator).to.be(true);
-      });
-
-      it('should provide a default display name', () => {
-        let manager = new MockContentsManager();
-        let creator = new DirectoryCreator(manager);
-        expect(creator instanceof DirectoryCreator).to.be(true);
-      });
-
-    });
-
-    describe('#createUntitled()', () => {
-
-      it('should create a new untitled directory on the given path', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new MyDirectoryCreator(manager);
-        creator.createUntitled('foo/').then(contents => {
-          expect(contents.type).to.be('directory');
-          expect(manager.methods.indexOf('newUntitled')).to.not.be(-1);
-          done();
-        });
-      });
-
-    });
-
-  });
-
-});

+ 0 - 125
test/src/filehandler/default.spec.ts

@@ -1,125 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import expect = require('expect.js');
-
-import {
-  IContentsModel, IContentsManager, IContentsOpts, ICheckpointModel,
-  IAjaxSettings, ContentsManager
-} from 'jupyter-js-services';
-
-import {
-  MockContentsManager
-} from 'jupyter-js-services/lib/mockcontents';
-
-import {
-  CodeMirrorWidget
-} from '../../../lib/codemirror/widget';
-
-import {
-  FileHandler
-} from '../../../lib/filehandler/default';
-
-
-class MyFileHandler extends FileHandler {
-
-  methods: string[] = [];
-
-  protected getSaveOptions(widget: CodeMirrorWidget, path: string): Promise<IContentsOpts> {
-    this.methods.push('getSaveOptions');
-    return super.getSaveOptions(widget, path);
-  }
-
-  protected createWidget(path: string): CodeMirrorWidget {
-    this.methods.push('createWidget');
-    return super.createWidget(path);
-  }
-
-  protected populateWidget(widget: CodeMirrorWidget, model: IContentsModel): Promise<IContentsModel> {
-    this.methods.push('populateWidget');
-    return super.populateWidget(widget, model);
-  }
-}
-
-
-describe('jupyter-ui', () => {
-
-  describe('FileHandler', () => {
-
-    describe('#constructor()', () => {
-
-      it('should accept a contents manager', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        expect(handler instanceof FileHandler).to.be(true);
-      });
-
-    });
-
-    describe('#createWidget()', () => {
-
-      it('should return a CodeMirrorWidget', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyFileHandler(manager);
-        let widget = handler.open('foo.txt');
-        expect(handler.methods.indexOf('createWidget')).to.not.be(-1);
-        expect(widget instanceof CodeMirrorWidget).to.be(true);
-      });
-
-
-      it('should set the dirty flag when the editor text changes', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyFileHandler(manager);
-        let widget = handler.open('foo.txt');
-        expect(handler.methods.indexOf('createWidget')).to.not.be(-1);
-        let editor = widget.editor;
-        editor.getDoc().setValue('test');
-        expect(handler.isDirty('foo.txt')).to.be(true);
-      });
-
-    });
-
-    describe('#populateWidget()', () => {
-
-      it('should load text and the appropriate codemirror mode', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyFileHandler(manager);
-        manager.createFile('foo.ts');
-        let widget = handler.open('foo.ts');
-        handler.finished.connect(() => {
-          expect(handler.methods.indexOf('populateWidget')).to.not.be(-1);
-          let doc = widget.editor.getDoc();
-          expect(doc.getValue()).to.be(manager.DEFAULT_TEXT);
-          let mode = doc.getMode();
-          expect(mode.name).to.be('javascript');
-          done();
-        });
-
-      });
-
-    });
-
-    describe('#getSaveOptions()', () => {
-
-      it('should save as a text file', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyFileHandler(manager);
-        manager.createFile('foo.ts');
-        let widget = handler.open('foo.ts');
-        widget.editor.getDoc().setValue('test test');
-        handler.save('foo.ts').then(contents => {
-          expect(handler.methods.indexOf('getSaveOptions')).to.not.be(-1);
-          expect(contents.path).to.be('foo.ts');
-          expect(contents.content).to.be('test test');
-          expect(contents.name).to.be('foo.ts');
-          expect(contents.type).to.be('file');
-          expect(contents.format).to.be('text');
-        });
-      });
-
-    });
-
-  });
-
-});

+ 0 - 582
test/src/filehandler/filehandler.spec.ts

@@ -1,582 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import expect = require('expect.js');
-
-import {
-  IContentsModel, IContentsManager, IContentsOpts, ICheckpointModel,
-  IAjaxSettings, ContentsManager
-} from 'jupyter-js-services';
-
-import {
-  MockContentsManager
-} from 'jupyter-js-services/lib/mockcontents';
-
-import {
-  sendMessage
-} from 'phosphor-messaging';
-
-import {
-  Widget
-} from 'phosphor-widget';
-
-import {
-  AbstractFileHandler
-} from '../../../lib/filehandler/handler';
-
-import {
-  acceptDialog, dismissDialog
-} from '../utils';
-
-
-class FileHandler extends AbstractFileHandler<Widget> {
-
-  methods: string[] = [];
-
-  protected getSaveOptions(widget: Widget, path: string): Promise<IContentsOpts> {
-    this.methods.push('getSaveOptions');
-    return Promise.resolve({ path, content: 'baz', name,
-                             type: 'file', format: 'text' });
-  }
-
-  protected createWidget(path: string): Widget {
-    this.methods.push('createWidget');
-    return new Widget();
-  }
-
-  protected populateWidget(widget: Widget, model: IContentsModel): Promise<IContentsModel> {
-    this.methods.push('populateWidget');
-    return Promise.resolve(model);
-  }
-
-  protected getFetchOptions(path: string): IContentsOpts {
-    this.methods.push('getFetchOptions');
-    return super.getFetchOptions(path);
-  }
-
-  protected getTitleText(path: string): string {
-    this.methods.push('getTitleText');
-    return super.getTitleText(path);
-  }
-
-  protected beforeClose(widget: Widget): Promise<void> {
-    this.methods.push('beforeClose');
-    return super.beforeClose(widget);
-  }
-}
-
-
-describe('jupyter-ui', () => {
-
-  describe('AbstractFileHandler', () => {
-
-    describe('#constructor()', () => {
-
-      it('should accept a contents manager', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        expect(handler instanceof AbstractFileHandler).to.be(true);
-      });
-
-    });
-
-    describe('#opened', () => {
-
-      it('should be emitted when an item is opened', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let called = false;
-        handler.opened.connect((h, widget) => {
-          expect(widget instanceof Widget).to.be(true);
-          called = true;
-        });
-        manager.createFile('foo.txt');
-        handler.open('foo.txt');
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#finished', () => {
-
-      it('should be emitted when a widget is populated', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        handler.finished.connect((h, widget) => {
-          expect(widget instanceof Widget).to.be(true);
-          done();
-        });
-        manager.createFile('foo.txt');
-        handler.open('foo.txt');
-      });
-
-    });
-
-    describe('#fileExtensions', () => {
-
-      it('should be an empty list by default', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        expect(handler.fileExtensions).to.eql([]);
-      });
-
-      it('should be read only', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        expect(() => { handler.fileExtensions = []; }).to.throwError();
-      });
-
-    });
-
-    describe('#findWidget()', () => {
-
-      it('should find a widget given a path', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(handler.findWidget('foo.txt')).to.be(widget);
-      });
-
-      it('should return `undefined` if the path is invalid', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(handler.findWidget('bar.txt')).to.be(void 0);
-      });
-
-    });
-
-    describe('#findPath()', () => {
-
-      it('should find a path given a widget', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(handler.findPath(widget)).to.be('foo.txt');
-      });
-
-      it('should return `undefined` if the widget is invalid', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.close('foo.txt').then(() => {
-          expect(handler.findPath(widget)).to.be(void 0);
-          done();
-        });
-      });
-
-    });
-
-    describe('#open()', () => {
-
-      it('should open a file by path and return a widget', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(widget instanceof Widget).to.be(true);
-      });
-
-      it('should return an existing widget if it is already open', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(handler.open('foo.txt')).to.be(widget);
-      });
-
-      it('should clear the dirty state when finished', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.finished.connect(() => {
-          expect(handler.isDirty('foo.txt')).to.be(false);
-          done();
-        });
-      });
-
-      it('should set the title', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(widget.title.text).to.be('foo.txt');
-      });
-
-    });
-
-    describe('#rename()', () => {
-
-      it('should rename the file', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        let result = handler.rename('foo.txt', 'bar.txt');
-        expect(result).to.be(true);
-        expect(handler.findWidget('bar.txt')).to.be(widget);
-      });
-
-      it('should update the title', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.rename('foo.txt', 'bar.txt');
-        expect(widget.title.text).to.be('bar.txt');
-      });
-
-      it('shoud be a no-op if the file is not found', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let result = handler.rename('foo.txt', 'bar.txt');
-        expect(result).to.be(false);
-      });
-
-      it('should close the widget if the new path is undefined', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        let result = handler.rename('foo.txt');
-        expect(result).to.be(true);
-        expect(handler.methods.indexOf('beforeClose')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#save()', () => {
-
-      it('should resolve to the file contents', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.save('foo.txt').then(contents => {
-          expect(contents.content).to.be('baz');
-          done();
-        });
-      });
-
-      it('should clear the dirty flag', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.setDirty('foo.txt', true);
-        handler.save('foo.txt').then(contents => {
-          expect(handler.isDirty('foo.txt')).to.be(false);
-          done();
-        });
-      });
-
-      it('should be a no-op if the file is not found', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        handler.save('foo.txt').then(contents => {
-          expect(contents).to.be(void 0);
-          done();
-        });
-      });
-
-    });
-
-    describe('#revert()', () => {
-
-      it('should resolve to the original file contents', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.revert('foo.txt').then(contents => {
-          expect(contents.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-      });
-
-      it('should clear the dirty flag', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        handler.setDirty('foo.txt', true);
-        handler.revert('foo.txt').then(contents => {
-          expect(handler.isDirty('foo.txt')).to.be(false);
-          done();
-        });
-      });
-
-      it('should be a no-op if the file is not found', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        handler.revert('foo.txt').then(contents => {
-          expect(contents).to.be(void 0);
-          done();
-        });
-      });
-
-    });
-
-    describe('#close()', () => {
-
-      it('should close a file by path', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        widget.attach(document.body);
-        handler.close('foo.txt').then(result => {
-          expect(result).to.be(true);
-          expect(widget.isAttached).to.be(false);
-          done();
-        });
-      });
-
-      it('should return false if the path is invalid', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        handler.close('foo.txt').then(result => {
-          expect(result).to.be(false);
-          done();
-        });
-      });
-
-      it('should prompt the user if the file is dirty', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        handler.open('foo.txt');
-        handler.setDirty('foo.txt', true);
-        handler.close('foo.txt').then(result => {
-          expect(result).to.be(true);
-          done();
-        });
-        acceptDialog();
-      });
-
-      it('should not close if the user dismisses the dialog', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        handler.open('foo.txt');
-        handler.setDirty('foo.txt', true);
-        handler.close('foo.txt').then(result => {
-          expect(result).to.be(false);
-          done();
-        });
-        dismissDialog();
-      });
-
-    });
-
-    describe('#closeAll()', () => {
-
-      it('should class all files', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        manager.createFile('bar.txt');
-        let widget0 = handler.open('foo.txt');
-        let widget1 = handler.open('bar.txt');
-        widget0.attach(document.body);
-        handler.closeAll().then(() => {
-          expect(widget0.isAttached).to.be(false);
-          expect(handler.findWidget('bar.txt')).to.be(void 0);
-          done();
-        });
-      });
-
-    });
-
-    describe('#isDirty()', () => {
-
-      it('should default to false', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        expect(handler.isDirty('foo.txt')).to.be(false);
-      });
-
-      it('should return `undefined` if the path is invalid', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        expect(handler.isDirty('bar.txt')).to.be(void 0);
-      });
-
-    });
-
-    describe('#setDirty()', () => {
-
-      it('should set the dirty state of a file', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.setDirty('foo.txt', true);
-        expect(handler.isDirty('foo.txt')).to.be(true);
-        handler.setDirty('foo.txt', false);
-        expect(handler.isDirty('foo.txt')).to.be(false);
-      });
-
-      it('should affect the className of the title', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        expect(widget.title.className.indexOf('jp-mod-dirty')).to.be(-1);
-        handler.setDirty('foo.txt', true);
-        expect(widget.title.className.indexOf('jp-mod-dirty')).to.not.be(-1);
-      });
-
-      it('should be a no-op for an invalid path', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.setDirty('bar.txt', true);
-      });
-
-    });
-
-    describe('#filterMessage()', () => {
-
-      it('should filter close messages for contained widgets', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        let value = handler.filterMessage(widget, Widget.MsgCloseRequest);
-        expect(value).to.be(true);
-        value = handler.filterMessage(widget, Widget.MsgUpdateRequest);
-        expect(value).to.be(false);
-      });
-
-    });
-
-    describe('#getFetchOptions()', () => {
-
-      it('should get the options use to fetch contents from disk', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        expect(handler.methods.indexOf('getFetchOptions')).to.not.be(-1);
-      });
-
-      it('should be called during a revert', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.methods = [];
-        handler.revert('foo.txt');
-        expect(handler.methods.indexOf('getFetchOptions')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#getSaveOptions()', () => {
-
-      it('should get the options used to save the widget', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.save('foo.txt');
-        expect(handler.methods.indexOf('getSaveOptions')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#createWidget()', () => {
-
-      it('should be used to create the initial widget given a path', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        expect(handler.methods.indexOf('createWidget')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#populateWidget()', () => {
-
-      it('should be called to populate a widget while opening', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.finished.connect(() => {
-          expect(handler.methods.indexOf('populateWidget')).to.not.be(-1);
-          done();
-        });
-      });
-
-      it('should be called when reverting', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        let called = false;
-        handler.finished.connect(() => {
-          handler.methods = [];
-          handler.revert('foo.txt').then(() => {
-            expect(handler.methods.indexOf('populateWidget')).to.not.be(-1);
-            done();
-          });
-        });
-      });
-
-    });
-
-    describe('#getTitleText()', () => {
-
-      it('should set the appropriate title text based on a path', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        expect(handler.methods.indexOf('getTitleText')).to.not.be(-1);
-      });
-
-      it('should be called when renaming', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget0 = handler.open('foo.txt');
-        handler.methods = [];
-        handler.rename('foo.txt', 'bar.txt');
-        expect(handler.methods.indexOf('getTitleText')).to.not.be(-1);
-      });
-    });
-
-    describe('#beforeClose()', () => {
-
-      it('should call before closing', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        manager.createFile('foo.txt');
-        let widget = handler.open('foo.txt');
-        widget.attach(document.body);
-        handler.close('foo.txt').then(result => {
-          expect(result).to.be(true);
-          expect(handler.methods.indexOf('beforeClose')).to.not.be(-1);
-          done();
-        });
-      });
-
-    });
-
-  });
-
-});

+ 0 - 495
test/src/filehandler/registry.spec.ts

@@ -1,495 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-'use strict';
-
-import expect = require('expect.js');
-
-import {
-  IContentsModel, IContentsManager, IContentsOpts, ICheckpointModel,
-  IAjaxSettings, ContentsManager
-} from 'jupyter-js-services';
-
-import {
-  MockContentsManager
-} from 'jupyter-js-services/lib/mockcontents';
-
-import {
-  Widget
-} from 'phosphor-widget';
-
-import {
-  CodeMirrorWidget
-} from '../../../lib/codemirror/widget';
-
-import {
-  AbstractFileHandler
-} from '../../../lib/filehandler/handler';
-
-import {
-  FileHandler
-} from '../../../lib/filehandler/default';
-
-import {
-  FileCreator
-} from '../../../lib/filehandler/creator';
-
-import {
-  FileHandlerRegistry
-} from '../../../lib/filehandler/registry';
-
-import {
-  acceptDialog, dismissDialog
-} from '../utils';
-
-
-class MyRegistry extends FileHandlerRegistry {
-
-  methods: string[] = [];
-  handlers: AbstractFileHandler<Widget>[] = [];
-
-  protected findHandler(path: string): AbstractFileHandler<Widget> {
-    this.methods.push('findHandler');
-    let value = super.findHandler(path);
-    this.handlers.push(value);
-    return value;
-  }
-}
-
-
-class MyHandler extends FileHandler {
-
-  methods: string[] = [];
-  handlers: AbstractFileHandler<Widget>[] = [];
-
-  get fileExtensions(): string[] {
-    return ['.txt'];
-  }
-
-  open(path: string): CodeMirrorWidget {
-    this.methods.push('open');
-    return super.open(path);
-  }
-
-}
-
-
-describe('jupyter-ui', () => {
-
-  describe('FileHandlerRegistry', () => {
-
-    describe('#opened()', () => {
-
-      it('should be emitted when a file is opened', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        let called = false;
-        registry.opened.connect((reg, widget) => {
-          expect(widget instanceof Widget).to.be(true);
-          called = true;
-        });
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#finished()', () => {
-
-      it('should be emitted when a file is finished opening', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        registry.finished.connect((reg, widget) => {
-          expect(widget instanceof Widget).to.be(true);
-          done();
-        });
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-      });
-
-    });
-
-    describe('#created()', () => {
-
-      it('should be emitted when a file is created', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.created.connect((reg, model) => {
-          expect(model.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-        registry.createNew('file', '/');
-        acceptDialog();
-      });
-
-    });
-
-    describe('#addHandler()', () => {
-
-      it('should add a handler to the registry', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(value).to.be(void 0);
-      });
-
-      it('should handle files that match its extension', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(value instanceof Widget).to.be(true);
-      });
-
-    });
-
-    describe('#addDefaultHandler()', () => {
-
-      it('should add a default handler to the registry', () => {
-        let manager = new MockContentsManager();
-        let handler = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(value instanceof Widget).to.be(true);
-      });
-
-      it('should be overruled by a handler with a matching extension', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        let main = new FileHandler(manager);
-        registry.addDefaultHandler(main);
-        registry.addHandler(handler);
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(handler.methods.indexOf('open')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#addCreator()', () => {
-
-      it('should add a creator for a specific type', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.created.connect((reg, model) => {
-          expect(model.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-        registry.createNew('file', '/');
-        acceptDialog();
-      });
-
-      it('should reject the promise if the type is not registered', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.createNew('directory', '/').catch(error => {
-          done();
-        });
-      });
-
-    });
-
-    describe('#listCreator()', () => {
-
-      it('should get the list of creator names', () => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.addCreator('foo', creator.createNew.bind(creator));
-        expect(registry.listCreators()).to.eql(['file', 'foo']);
-      });
-
-    });
-
-    describe('#createNew()', () => {
-
-      it('should create a new file', (done) => {
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.created.connect((reg, model) => {
-          expect(model.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-        registry.createNew('file', '/');
-        acceptDialog();
-      });
-
-      it('should accept a host for the dialog', (done) => {
-        let node = document.createElement('div');
-        document.body.appendChild(node);
-        let manager = new MockContentsManager();
-        let creator = new FileCreator(manager);
-        let registry = new MyRegistry();
-        registry.addCreator('file', creator.createNew.bind(creator));
-        registry.created.connect((reg, model) => {
-          expect(model.content).to.be(manager.DEFAULT_TEXT);
-          document.body.removeChild(node);
-          done();
-        });
-        registry.createNew('file', '/', node);
-        acceptDialog(node);
-      });
-
-    });
-
-    describe('#open()', () => {
-
-      it('should open a file by path', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        let called = false;
-        registry.opened.connect(() => {
-          called = true;
-        });
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(value instanceof Widget).to.be(true);
-        registry.finished.connect((r, w) => {
-          expect(called).to.be(true);
-          expect(w).to.be(value);
-          done();
-        });
-      });
-
-      it('should bail if there is no handler', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        manager.createFile('foo.txt');
-        let value = registry.open('foo.txt');
-        expect(value).to.be(void 0);
-      });
-
-    });
-
-    describe('#rename()', () => {
-
-      it('should rename the appropriate widget', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        let widget = registry.open('foo.txt');
-        let response = registry.rename('foo.txt', 'bar.txt');
-        expect(widget.title.text).to.be('bar.txt');
-        expect(response).to.be(true);
-      });
-
-      it('should bail if the path is not opened', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        let response = registry.rename('foo.txt');
-        expect(response).to.be(false);
-      });
-
-    });
-
-    describe('#save()', () => {
-
-      it('should save the appropriate widget', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        registry.save('foo.txt').then(contents => {
-          expect(contents.name).to.be('foo.txt')
-          done();
-        });
-      });
-
-      it('should reject if the path is not opened', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.save('foo.txt').catch(error => {
-          done();
-        });
-      });
-
-    });
-
-    describe('#revert()', () => {
-
-      it('should revert the appropriate widget', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        registry.revert('foo.txt').then(contents => {
-          expect(contents.name).to.be('foo.txt');
-          expect(contents.content).to.be(manager.DEFAULT_TEXT);
-          done();
-        });
-      });
-
-      it('should reject if the path is not opened', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.revert('foo.txt').catch(error => {
-          done();
-        });
-      });
-
-    });
-
-    describe('#close()', () => {
-
-      it('should close the appropriate widget', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        registry.close('foo.txt').then(value => {
-          expect(value).to.be(true);
-          done();
-        });
-      });
-
-      it('should reject if the path is not opened', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.close('foo.txt').catch(error => {
-          done();
-        });
-      });
-
-    });
-
-    describe('#closeAll', () => {
-
-      it('should close all open widgets across openers', (done) => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        let main = new FileHandler(manager);
-        registry.addDefaultHandler(main);
-        registry.addHandler(handler);
-        manager.createFile('foo.txt');
-        manager.createFile('foo.md');
-        let widget0 = registry.open('foo.txt');
-        let widget1 = registry.open('foo.md');
-        registry.closeAll().then(() => {
-          expect(registry.findPath(widget0)).to.be(void 0);
-          expect(registry.findPath(widget1)).to.be(void 0);
-          done();
-        });
-      });
-
-    });
-
-    describe('#findPath()', () => {
-
-      it('should get the path for a given widget', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        let widget = registry.open('foo.txt');
-        expect(registry.findPath(widget)).to.be('foo.txt');
-      });
-
-      it('should return `undefined` if not found', () => {
-        let registry = new MyRegistry();
-        let widget = new Widget();
-        expect(registry.findPath(widget)).to.be(void 0);
-      });
-
-    });
-
-    describe('#findWidget()', () => {
-
-      it('should get the widget for a given path', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        let widget = registry.open('foo.txt');
-        expect(registry.findWidget('foo.txt')).to.be(widget);
-      });
-
-      it('should return `undefined` if not found', () => {
-        let registry = new MyRegistry();
-        expect(registry.findWidget('foo.txt')).to.be(void 0);
-      });
-
-    });
-
-    describe('#findHandler()', () => {
-
-      it('should use a lone registered handler', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        expect(registry.methods.indexOf('findHandler')).to.not.be(-1);
-        expect(registry.handlers.indexOf(handler)).to.not.be(-1);
-      });
-
-      it('should use the default handler if there is more than one', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let main = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        registry.addDefaultHandler(main);
-        manager.createFile('foo.md');
-        registry.open('foo.md');
-        expect(registry.methods.indexOf('findHandler')).to.not.be(-1);
-        expect(registry.handlers.indexOf(main)).to.not.be(-1);
-      });
-
-      it('should use the more specifc handler by filename', () => {
-        let manager = new MockContentsManager();
-        let handler = new MyHandler(manager);
-        let main = new FileHandler(manager);
-        let registry = new MyRegistry();
-        registry.addHandler(handler);
-        registry.addDefaultHandler(handler);
-        manager.createFile('foo.txt');
-        registry.open('foo.txt');
-        expect(registry.methods.indexOf('findHandler')).to.not.be(-1);
-        expect(registry.handlers.indexOf(handler)).to.not.be(-1);
-      });
-
-    });
-
-  });
-
-});

+ 0 - 4
test/src/index.ts

@@ -3,10 +3,6 @@
 'use strict';
 
 import './dialog/dialog.spec';
-import './filehandler/creator.spec';
-import './filehandler/default.spec';
-import './filehandler/filehandler.spec';
-import './filehandler/registry.spec';
 import './renderers/renderers.spec';
 import './rendermime/rendermime.spec';
 import './renderers/latex.spec';