Переглянути джерело

Remove the context manager

Steven Silvester 8 роки тому
батько
коміт
9c62a505a9

+ 176 - 426
src/docmanager/context.ts

@@ -10,7 +10,7 @@ import {
 } from 'phosphor/lib/algorithm/searching';
 
 import {
-  IDisposable
+  IDisposable, DisposableDelegate
 } from 'phosphor/lib/core/disposable';
 
 import {
@@ -36,50 +36,50 @@ import {
 
 /**
  * An implementation of a document context.
+ *
+ * This class is typically instantiated by the document manger.
  */
-class Context implements IDocumentContext<IDocumentModel> {
+export
+class Context<T extends IDocumentModel> implements IDocumentContext<T> {
   /**
    * Construct a new document context.
    */
-  constructor(manager: ContextManager) {
-    this._manager = manager;
-    this._id = utils.uuid();
+  constructor(options: Context.IOptions<T>) {
+    let manager = this._manager = options.manager;
+    this._factory = options.factory;
+    this._opener = options.opener;
+    this._path = options.path;
+    let lang = this._factory.preferredLanguage(this._path);
+    this._model = this._factory.createNew(lang);
+    manager.sessions.runningChanged.connect(this._onSessionsChanged, this);
+    this._saver = new SaveHandler({ context: this, manager });
+    this._saver.start();
   }
 
   /**
    * A signal emitted when the kernel changes.
    */
-  kernelChanged: ISignal<Context, IKernel>;
+  kernelChanged: ISignal<IDocumentContext<T>, IKernel>;
 
   /**
    * A signal emitted when the path changes.
    */
-  pathChanged: ISignal<Context, string>;
+  pathChanged: ISignal<IDocumentContext<T>, string>;
 
   /**
    * A signal emitted when the model is saved or reverted.
    */
-  contentsModelChanged: ISignal<Context, IContents.IModel>;
+  contentsModelChanged: ISignal<IDocumentContext<T>, IContents.IModel>;
 
   /**
    * A signal emitted when the context is fully populated for the first time.
    */
-  populated: ISignal<IDocumentContext<IDocumentModel>, void>;
+  populated: ISignal<IDocumentContext<T>, void>;
 
   /**
    * A signal emitted when the context is disposed.
    */
-  disposed: ISignal<IDocumentContext<IDocumentModel>, void>;
-
-  /**
-   * The unique id of the context.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get id(): string {
-    return this._id;
-  }
+  disposed: ISignal<IDocumentContext<T>, void>;
 
   /**
    * Get the model associated with the document.
@@ -87,8 +87,8 @@ class Context implements IDocumentContext<IDocumentModel> {
    * #### Notes
    * This is a read-only property
    */
-  get model(): IDocumentModel {
-    return this._manager.getModel(this._id);
+  get model(): T {
+    return this._model;
   }
 
   /**
@@ -98,17 +98,14 @@ class Context implements IDocumentContext<IDocumentModel> {
    * This is a read-only propery.
    */
   get kernel(): IKernel {
-    return this._manager.getKernel(this._id);
+    return this._session ? this._session.kernel : null;
   }
 
   /**
    * The current path associated with the document.
-   *
-   * #### Notes
-   * This is a read-only property.
    */
   get path(): string {
-    return this._manager.getPath(this._id);
+    return this._path;
   }
 
   /**
@@ -119,7 +116,7 @@ class Context implements IDocumentContext<IDocumentModel> {
    * empty `contents` field.
    */
   get contentsModel(): IContents.IModel {
-    return this._manager.getContentsModel(this._id);
+    return this._contentsModel;
   }
 
   /**
@@ -129,7 +126,7 @@ class Context implements IDocumentContext<IDocumentModel> {
    * This is a read-only property.
    */
   get kernelspecs(): IKernel.ISpecModels {
-    return this._manager.getKernelspecs();
+    return this._manager.kernelspecs;
   }
 
   /**
@@ -139,292 +136,63 @@ class Context implements IDocumentContext<IDocumentModel> {
    * This is a read-only property.
    */
   get isPopulated(): boolean {
-    return this._manager.isPopulated(this._id);
-  }
-
-  /**
-   * Test whether the context has been disposed (read-only).
-   */
-  get isDisposed(): boolean {
-    return this._manager === null;
-  }
-
-  /**
-   * Dispose of the resources held by the context.
-   */
-  dispose(): void {
-    if (this.isDisposed) {
-      return;
-    }
-    this.disposed.emit(void 0);
-    clearSignalData(this);
-    this._manager.removeContext(this.id);
-    this._manager = null;
-  }
-
-  /**
-   * Change the current kernel associated with the document.
-   */
-  changeKernel(options: IKernel.IModel): 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 chosen by the user.
-   */
-  saveAs(): Promise<void> {
-    return this._manager.saveAs(this._id);
-  }
-
-  /**
-   * Revert the document contents to disk contents.
-   */
-  revert(): Promise<void> {
-    return this._manager.revert(this._id);
-  }
-
-  /**
-   * Create a checkpoint for the file.
-   */
-  createCheckpoint(): Promise<IContents.ICheckpointModel> {
-    return this._manager.createCheckpoint(this._id);
-  }
-
-  /**
-   * Delete a checkpoint for the file.
-   */
-  deleteCheckpoint(checkpointID: string): Promise<void> {
-    return this._manager.deleteCheckpoint(this.id, checkpointID);
-  }
-
-  /**
-   * Restore the file to a known checkpoint state.
-   */
-  restoreCheckpoint(checkpointID?: string): Promise<void> {
-    return this._manager.restoreCheckpoint(this.id, checkpointID);
-  }
-
-  /**
-   * List available checkpoints for the file.
-   */
-  listCheckpoints(): Promise<IContents.ICheckpointModel[]> {
-    return this._manager.listCheckpoints(this.id);
-  }
-
-  /**
-   * Get the list of running sessions.
-   */
-  listSessions(): Promise<ISession.IModel[]> {
-    return this._manager.listSessions();
-  }
-
-  /**
-   * Resolve a url to a correct server path.
-   */
-  resolveUrl(url: string): string {
-    return this._manager.resolveUrl(this._id, url);
+    return this._isPopulated;
   }
 
   /**
-   * 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.
+   * Get the model factory name.
    *
    * #### 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;
-}
-
-
-// Define the signals for the `Context` class.
-defineSignal(Context.prototype, 'kernelChanged');
-defineSignal(Context.prototype, 'pathChanged');
-defineSignal(Context.prototype, 'contentsModelChanged');
-defineSignal(Context.prototype, 'populated');
-defineSignal(Context.prototype, 'disposed');
-
-
-/**
- * An object which manages the active contexts.
- */
-export
-class ContextManager implements IDisposable {
-  /**
-   * Construct a new context manager.
+   * This is a read-only property.
    */
-  constructor(options: ContextManager.IOptions) {
-    let manager = this._manager = options.manager;
-    this._opener = options.opener;
-    manager.sessions.runningChanged.connect(this._onSessionsChanged, this);
+  get factoryName(): string {
+    return this.isDisposed ? '' : this._factory.name;
   }
 
   /**
-   * Get whether the context manager has been disposed.
+   * Test whether the context has been disposed (read-only).
    */
   get isDisposed(): boolean {
     return this._manager === null;
   }
 
   /**
-   * Dispose of the resources held by the document manager.
+   * Dispose of the resources held by the context.
    */
   dispose(): void {
     if (this.isDisposed) {
       return;
     }
+    this.disposed.emit(void 0);
+    clearSignalData(this);
+    this._model.dispose();
     this._manager = null;
-    for (let id in this._contexts) {
-      this._contexts[id].context.dispose();
-    }
-    this._contexts = null;
-    this._opener = null;
-  }
-
-  /**
-   * Create a new context.
-   */
-  createNew(path: string, model: IDocumentModel, factory: IModelFactory): string {
-    let context = new Context(this);
-    let saveHandler = new SaveHandler({ context, services: this._manager });
-    saveHandler.start();
-    let id = context.id;
-    this._contexts[id] = {
-      context,
-      path,
-      model,
-      modelName: factory.name,
-      fileType: factory.fileType,
-      fileFormat: factory.fileFormat,
-      contentsModel: null,
-      session: null,
-      isPopulated: false,
-      saveHandler
-    };
-    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<IDocumentModel> {
-    return this._contexts[id].context;
-  }
-
-  /**
-   * Get the model associated with a context.
-   */
-  getModel(id: string): IDocumentModel {
-    return this._contexts[id].model;
-  }
-
-  /**
-   * Remove a context that has been disposed.
-   *
-   * #### Notes
-   * This is called when a context is disposed,
-   * and should not be called by user code.
-   */
-  removeContext(id: string): void {
-    let contextEx = this._contexts[id];
-    contextEx.model.dispose();
-    contextEx.saveHandler.dispose();
-    let session = contextEx.session;
-    if (session) {
-      session.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): IContents.IModel {
-    return this._contexts[id].contentsModel;
+    this._factory = null;
   }
 
   /**
    * Change the current kernel associated with the document.
-   *
-   * @param options - If given, change the kernel (starting a session
-   * if necessary). If falsey, shut down any existing session and return
    */
-  changeKernel(id: string, options: IKernel.IModel): Promise<IKernel> {
-    let contextEx = this._contexts[id];
-    let session = contextEx.session;
+  changeKernel(options: IKernel.IModel): Promise<IKernel> {
+    let session = this._session;
     if (options) {
       if (session) {
         return session.changeKernel(options);
       } else {
-        let path = contextEx.path;
+        let path = this._path;
         let sOptions: ISession.IOptions = {
-          path: path,
+          path,
           kernelName: options.name,
           kernelId: options.id
         };
-        return this._startSession(id, sOptions);
+        return this._startSession(sOptions);
       }
     } else {
       if (session) {
         return session.shutdown().then(() => {
           session.dispose();
-          contextEx.session = null;
-          contextEx.context.kernelChanged.emit(null);
+          this._session = null;
+          this.kernelChanged.emit(null);
           return void 0;
         });
       } else {
@@ -434,48 +202,31 @@ class ContextManager implements IDisposable {
   }
 
   /**
-   * Update the path of an open document.
-   *
-   * @param id - The id of the context.
+   * Set the path of the context.
    *
-   * @param newPath - The new path.
-   */
-  handleRename(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.rename(newPath);
-          sessionUpdated = true;
-        }
-      }
-    }
-  }
-
-  /**
-   * Get the current kernelspec information.
+   * #### Notes
+   * This should only be called by the document manager.
+   * It is assumed that the file has been renamed on the
+   * contents manager outside of this operation.
    */
-  getKernelspecs(): IKernel.ISpecModels {
-    return this._manager.kernelspecs;
+  setPath(value: string): void {
+    this._path = value;
+    let session = this._session;
+    if (session) {
+      session.rename(value);
+    }
+    this.pathChanged.emit(value);
   }
 
   /**
    * Save the document contents to disk.
    */
-  save(id: string): Promise<void> {
-    let contextEx =  this._contexts[id];
-    let model = contextEx.model;
-    let contents = contextEx.contentsModel || {};
-    let path = contextEx.path;
-    contents.type = contextEx.fileType;
-    contents.format = contextEx.fileFormat;
+  save(): Promise<void> {
+    let model = this._model;
+    let contents = this._contentsModel || {};
+    let path = this._path;
+    contents.type = this._factory.fileType;
+    contents.format = this._factory.fileFormat;
     if (model.readOnly) {
       return Promise.reject(new Error('Read only'));
     }
@@ -485,8 +236,11 @@ class ContextManager implements IDisposable {
       contents.content = model.toString();
     }
     return this._manager.contents.save(path, contents).then(newContents => {
-      contextEx.contentsModel = this._copyContentsModel(newContents);
+      this._contentsModel = this._copyContentsModel(newContents);
       model.dirty = false;
+      if (!this._isPopulated) {
+        this._populate();
+      }
     }).catch(err => {
       showDialog({
         title: 'File Save Error',
@@ -497,44 +251,40 @@ class ContextManager implements IDisposable {
   }
 
   /**
-   * Save a document to a new file path chosen by the user.
-   *
-   * This results in a new session.
+   * Save the document to a different path chosen by the user.
    */
-  saveAs(id: string): Promise<void> {
-    let contextEx = this._contexts[id];
-    return Private.saveAs(contextEx.path).then(newPath => {
+  saveAs(): Promise<void> {
+    return Private.getSavePath(this._path).then(newPath => {
       if (!newPath) {
         return;
       }
-      contextEx.path = newPath;
-      contextEx.context.pathChanged.emit(newPath);
-      if (contextEx.session) {
+      this.setPath(newPath);
+      let session = this._session;
+      if (session) {
         let options: ISession.IOptions = {
           path: newPath,
-          kernelId: contextEx.session.kernel.id,
-          kernelName: contextEx.session.kernel.name
+          kernelId: session.kernel.id,
+          kernelName: session.kernel.name
         };
-        return this._startSession(id, options).then(() => {
-          return this.save(id);
+        return this._startSession(options).then(() => {
+          return this.save();
         });
       }
-      return this.save(id);
+      return this.save();
     });
   }
 
   /**
-   * Revert the contents of a path.
+   * Revert the document contents to disk contents.
    */
-  revert(id: string): Promise<void> {
-    let contextEx = this._contexts[id];
+  revert(): Promise<void> {
     let opts: IContents.IFetchOptions = {
-      format: contextEx.fileFormat,
-      type: contextEx.fileType,
+      format: this._factory.fileFormat,
+      type: this._factory.fileType,
       content: true
     };
-    let path = contextEx.path;
-    let model = contextEx.model;
+    let path = this._path;
+    let model = this._model;
     return this._manager.contents.get(path, opts).then(contents => {
       if (contents.format === 'json') {
         model.fromJSON(contents.content);
@@ -542,10 +292,14 @@ class ContextManager implements IDisposable {
         model.fromString(contents.content);
       }
       let contentsModel = this._copyContentsModel(contents);
-      // TODO: use deepEqual to check for equality
-      contextEx.contentsModel = contentsModel;
-      contextEx.context.contentsModelChanged.emit(contentsModel);
+      this._contentsModel = contentsModel;
+      if (contentsModel.last_modified !== this._contentsModel.last_modified) {
+        this.contentsModelChanged.emit(contentsModel);
+      }
       model.dirty = false;
+      if (!this._isPopulated) {
+        this._populate();
+      }
     }).catch(err => {
       showDialog({
         title: 'File Load Error',
@@ -556,64 +310,42 @@ class ContextManager implements IDisposable {
   }
 
   /**
-   * Test whether the context is fully populated.
-   */
-  isPopulated(id: string): boolean {
-    let contextEx = this._contexts[id];
-    return contextEx.isPopulated;
-  }
-
-  /**
-   * Finalize a context.
-   */
-  finalize(id: string): void {
-    let contextEx = this._contexts[id];
-    if (contextEx.isPopulated) {
-      return;
-    }
-    contextEx.isPopulated = true;
-    this._contexts[id].context.populated.emit(void 0);
-  }
-
-  /**
-   * Create a checkpoint for a file.
+   * Create a checkpoint for the file.
    */
-  createCheckpoint(id: string): Promise<IContents.ICheckpointModel> {
-    let path = this._contexts[id].path;
-    return this._manager.contents.createCheckpoint(path);
+  createCheckpoint(): Promise<IContents.ICheckpointModel> {
+    return this._manager.contents.createCheckpoint(this._path);
   }
 
   /**
-   * Delete a checkpoint for a file.
+   * Delete a checkpoint for the file.
    */
-  deleteCheckpoint(id: string, checkpointID: string): Promise<void> {
-    let path = this._contexts[id].path;
-    return this._manager.contents.deleteCheckpoint(path, checkpointID);
+  deleteCheckpoint(checkpointID: string): Promise<void> {
+    return this._manager.contents.deleteCheckpoint(this._path, checkpointID);
   }
 
   /**
-   * Restore a file to a known checkpoint state.
+   * Restore the file to a known checkpoint state.
    */
-  restoreCheckpoint(id: string, checkpointID?: string): Promise<void> {
-    let path = this._contexts[id].path;
+  restoreCheckpoint(checkpointID?: string): Promise<void> {
+    let contents = this._manager.contents;
+    let path = this._path;
     if (checkpointID) {
-      return this._manager.contents.restoreCheckpoint(path, checkpointID);
+      return contents.restoreCheckpoint(path, checkpointID);
     }
-    return this.listCheckpoints(id).then(checkpoints => {
+    return this.listCheckpoints().then(checkpoints => {
       if (!checkpoints.length) {
         return;
       }
       checkpointID = checkpoints[checkpoints.length - 1].id;
-      return this._manager.contents.restoreCheckpoint(path, checkpointID);
+      return contents.restoreCheckpoint(path, checkpointID);
     });
   }
 
   /**
    * List available checkpoints for a file.
    */
-  listCheckpoints(id: string): Promise<IContents.ICheckpointModel[]> {
-    let path = this._contexts[id].path;
-    return this._manager.contents.listCheckpoints(path);
+  listCheckpoints(): Promise<IContents.ICheckpointModel[]> {
+    return this._manager.contents.listCheckpoints(this._path);
   }
 
   /**
@@ -626,13 +358,12 @@ class ContextManager implements IDisposable {
   /**
    * Resolve a relative url to a correct server path.
    */
-  resolveUrl(id: string, url: string): string {
+  resolveUrl(url: string): string {
     // Ignore urls that have a protocol.
     if (utils.urlParse(url).protocol || url.indexOf('//') === 0) {
       return url;
     }
-    let contextEx = this._contexts[id];
-    let cwd = ContentsManager.dirname(contextEx.path);
+    let cwd = ContentsManager.dirname(this._path);
     let path = ContentsManager.getAbsolutePath(url, cwd);
     return this._manager.contents.getDownloadUrl(path);
   }
@@ -640,31 +371,31 @@ class ContextManager implements IDisposable {
   /**
    * Add a sibling widget to the document manager.
    */
-  addSibling(id: string, widget: Widget): IDisposable {
+  addSibling(widget: Widget): IDisposable {
     let opener = this._opener;
-    return opener(id, widget);
+    opener(widget);
+    return new DisposableDelegate(() => {
+      widget.close();
+    });
   }
 
   /**
    * Start a session and set up its signals.
    */
-  private _startSession(id: string, options: ISession.IOptions): Promise<IKernel> {
-    let contextEx = this._contexts[id];
-    let context = contextEx.context;
+  private _startSession(options: ISession.IOptions): Promise<IKernel> {
     return this._manager.sessions.startNew(options).then(session => {
-      if (contextEx.session) {
-        contextEx.session.dispose();
+      if (this._session) {
+        this._session.dispose();
       }
-      contextEx.session = session;
-      context.kernelChanged.emit(session.kernel);
+      this._session = session;
+      this.kernelChanged.emit(session.kernel);
       session.pathChanged.connect((s, path) => {
-        if (path !== contextEx.path) {
-          contextEx.path = path;
-          context.pathChanged.emit(path);
+        if (path !== this._path) {
+          this.setPath(path);
         }
       });
       session.kernelChanged.connect((s, kernel) => {
-        context.kernelChanged.emit(kernel);
+        this.kernelChanged.emit(kernel);
       });
       return session.kernel;
     });
@@ -690,36 +421,62 @@ class ContextManager implements IDisposable {
    * Handle a change to the running sessions.
    */
   private _onSessionsChanged(sender: ISession.IManager, models: ISession.IModel[]): void {
-    for (let id in this._contexts) {
-      let contextEx = this._contexts[id];
-      let session = contextEx.session;
-      if (!session) {
-        continue;
-      }
-      let index = findIndex(models, model => model.id === session.id);
-      if (index === -1) {
-        session.dispose();
-        contextEx.session = null;
-        contextEx.context.kernelChanged.emit(null);
-      }
+    let session = this._session;
+    if (!session) {
+      return;
+    }
+    let index = findIndex(models, model => model.id === session.id);
+    if (index === -1) {
+      session.dispose();
+      this._session = null;
+      this.kernelChanged.emit(null);
     }
   }
 
+  /**
+   * Handle an initial population.
+   */
+  private _populate(): void {
+    this._isPopulated = true;
+    // Add a checkpoint if none exists.
+    this.listCheckpoints().then(checkpoints => {
+      if (!checkpoints) {
+        return this.createCheckpoint();
+      }
+    }).then(() => {
+      this.populated.emit(void 0);
+    });
+  }
+
   private _manager: IServiceManager = null;
-  private _contexts: { [key: string]: Private.IContextEx } = Object.create(null);
-  private _opener: (id: string, widget: Widget) => IDisposable = null;
+  private _opener: (widget: Widget) => void = null;
+  private _model: T = null;
+  private _path = '';
+  private _session: ISession = null;
+  private _factory: IModelFactory<T> = null;
+  private _saver: SaveHandler = null;
+  private _isPopulated = false;
+  private _contentsModel: IContents.IModel = null;
 }
 
 
+// Define the signals for the `Context` class.
+defineSignal(Context.prototype, 'kernelChanged');
+defineSignal(Context.prototype, 'pathChanged');
+defineSignal(Context.prototype, 'contentsModelChanged');
+defineSignal(Context.prototype, 'populated');
+defineSignal(Context.prototype, 'disposed');
+
+
 /**
- * A namespace for ContextManager statics.
+ * A namespace for `Context` statics.
  */
-export namespace ContextManager {
+export namespace Context {
   /**
-   * The options used to initialize a context manager.
+   * The options used to initialize a context.
    */
   export
-  interface IOptions {
+  interface IOptions<T extends IDocumentModel> {
     /**
      * A service manager instance.
      */
@@ -728,7 +485,17 @@ export namespace ContextManager {
     /**
      * A callback for opening sibling widgets.
      */
-    opener: (id: string, widget: Widget) => IDisposable;
+    opener: (widget: Widget) => void;
+
+    /**
+     * The model factory used to create the model.
+     */
+    factory: IModelFactory<T>;
+
+    /**
+     * The initial path of the file.
+     */
+    path: string;
   }
 }
 
@@ -737,28 +504,11 @@ export namespace ContextManager {
  * A namespace for private data.
  */
 namespace Private {
-  /**
-   * An extended interface for data associated with a context.
-   */
-  export
-  interface IContextEx {
-    context: IDocumentContext<IDocumentModel>;
-    model: IDocumentModel;
-    session: ISession;
-    fileType: IContents.FileType;
-    fileFormat: IContents.FileFormat;
-    path: string;
-    contentsModel: IContents.IModel;
-    modelName: string;
-    isPopulated: boolean;
-    saveHandler: SaveHandler;
-  }
-
   /**
    * Get a new file path from the user.
    */
   export
-  function saveAs(path: string): Promise<string> {
+  function getSavePath(path: string): Promise<string> {
     let input = document.createElement('input');
     input.value = path;
     return showDialog({

+ 88 - 57
src/docmanager/manager.ts

@@ -6,7 +6,19 @@ import {
 } from 'jupyter-js-services';
 
 import {
-  IDisposable, DisposableDelegate
+  each
+} from 'phosphor/lib/algorithm/iteration';
+
+import {
+  find
+} from 'phosphor/lib/algorithm/searching';
+
+import {
+  Vector
+} from 'phosphor/lib/collections/vector';
+
+import {
+  IDisposable
 } from 'phosphor/lib/core/disposable';
 
 import {
@@ -15,7 +27,7 @@ import {
 
 import {
   IDocumentRegistry, IWidgetFactory, IWidgetFactoryOptions,
-  IDocumentModel, IDocumentContext
+  IDocumentModel, IDocumentContext, IModelFactory
 } from '../docregistry';
 
 import {
@@ -23,7 +35,7 @@ import {
 } from '../filebrowser';
 
 import {
-  ContextManager
+  Context
 } from './context';
 
 import {
@@ -49,19 +61,8 @@ class DocumentManager implements IDisposable {
   constructor(options: DocumentManager.IOptions) {
     this._registry = options.registry;
     this._serviceManager = options.manager;
-    let opener = options.opener;
-    this._contextManager = new ContextManager({
-      manager: this._serviceManager,
-      opener: (id: string, widget: Widget) => {
-        this._widgetManager.adoptWidget(id, widget);
-        opener.open(widget);
-        return new DisposableDelegate(() => {
-          widget.close();
-        });
-      }
-    });
+    this._opener = options.opener;
     this._widgetManager = new DocumentWidgetManager({
-      contextManager: this._contextManager,
       registry: this._registry
     });
   }
@@ -101,8 +102,10 @@ class DocumentManager implements IDisposable {
       return;
     }
     this._serviceManager = null;
-    this._contextManager.dispose();
-    this._contextManager = null;
+    each(this._contexts, context => {
+      context.dispose();
+    });
+    this._contexts.clear();
     this._widgetManager.dispose();
     this._widgetManager = null;
   }
@@ -121,32 +124,18 @@ class DocumentManager implements IDisposable {
     if (widgetName === 'default') {
       widgetName = registry.defaultWidgetFactory(ContentsManager.extname(path));
     }
-    let mFactory = registry.getModelFactoryFor(widgetName);
-    if (!mFactory) {
+    let factory = registry.getModelFactoryFor(widgetName);
+    if (!factory) {
       return;
     }
     // Use an existing context if available.
-    let id = this._contextManager.findContext(path, mFactory.name);
-    if (id) {
-      return this._widgetManager.createWidget(widgetName, id, kernel);
+    let context = this._findContext(path, factory.name);
+    if (!context) {
+      context = this._createContext(path, factory);
+      // Load the contents from disk.
+      context.revert();
     }
-    let lang = mFactory.preferredLanguage(path);
-    let model = mFactory.createNew(lang);
-    let ctxManager = this._contextManager;
-    id = ctxManager.createNew(path, model, mFactory);
-    // Load the contents from disk.
-    // Create a checkpoint if none exists.
-    ctxManager.listCheckpoints(id).then(checkpoints => {
-      if (!checkpoints) {
-        return ctxManager.createCheckpoint(id);
-      }
-    }).then(() => {
-      return ctxManager.revert(id);
-    }).then(() => {
-      model.dirty = false;
-      this._contextManager.finalize(id);
-    });
-    return this._widgetManager.createWidget(widgetName, id, kernel);
+    return this._widgetManager.createWidget(widgetName, context, kernel);
   }
 
   /**
@@ -163,21 +152,14 @@ class DocumentManager implements IDisposable {
     if (widgetName === 'default') {
       widgetName = registry.defaultWidgetFactory(ContentsManager.extname(path));
     }
-    let mFactory = registry.getModelFactoryFor(widgetName);
-    if (!mFactory) {
+    let factory = registry.getModelFactoryFor(widgetName);
+    if (!factory) {
       return;
     }
-    let lang = mFactory.preferredLanguage(path);
-    let model = mFactory.createNew(lang);
-    let ctxManager = this._contextManager;
-    let id = ctxManager.createNew(path, model, mFactory);
-    ctxManager.save(id).then(() => {
-      return ctxManager.createCheckpoint(id);
-    }).then(() => {
-      model.dirty = false;
-      ctxManager.finalize(id);
-    });
-    return this._widgetManager.createWidget(widgetName, id, kernel);
+    let context = this._createContext(path, factory);
+    // Immediately save the contents to disk.
+    context.save();
+    return this._widgetManager.createWidget(widgetName, context, kernel);
   }
 
   /**
@@ -195,7 +177,11 @@ class DocumentManager implements IDisposable {
    * @param newPath - The new path.
    */
   handleRename(oldPath: string, newPath: string): void {
-    this._contextManager.handleRename(oldPath, newPath);
+    each(this._contexts, context => {
+      if (context.path === oldPath) {
+        context.setPath(newPath);
+      }
+    });
   }
 
   /**
@@ -220,7 +206,8 @@ class DocumentManager implements IDisposable {
     if (widgetName === 'default') {
       widgetName = this._registry.defaultWidgetFactory(ContentsManager.extname(path));
     }
-    return this._widgetManager.findWidget(path, widgetName);
+    let context = this._contextForPath(path);
+    return this._widgetManager.findWidget(context, widgetName);
   }
 
   /**
@@ -245,20 +232,64 @@ class DocumentManager implements IDisposable {
    * Close the widgets associated with a given path.
    */
   closeFile(path: string): void {
-    this._widgetManager.closeFile(path);
+    let context = this._contextForPath(path);
+    this._widgetManager.close(context);
   }
 
   /**
    * Close all of the open documents.
    */
   closeAll(): void {
-    this._widgetManager.closeAll();
+    each(this._contexts, context => {
+      this._widgetManager.close(context);
+    });
+  }
+
+  /**
+   * Find a context for a given path and factory name.
+   */
+  private _findContext(path: string, factoryName: string): Context<IDocumentModel> {
+    return find(this._contexts, context => {
+      return (context.factoryName === factoryName &&
+              context.path === path);
+    });
+  }
+
+  /**
+   * Get a context for a given path.
+   */
+  private _contextForPath(path: string): Context<IDocumentModel> {
+    return find(this._contexts, context => {
+      return context.path === path;
+    });
+  }
+
+  /**
+   * Create a context from a path and a model factory.
+   */
+  private _createContext(path: string, factory: IModelFactory<IDocumentModel>): Context<IDocumentModel> {
+    let adopter = (widget: Widget) => {
+      this._widgetManager.adoptWidget(context, widget);
+      this._opener.open(widget);
+    };
+    let context = new Context({
+      opener: adopter,
+      manager: this._serviceManager,
+      factory,
+      path
+    });
+    context.disposed.connect(() => {
+      this._contexts.remove(context);
+    });
+    this._contexts.pushBack(context);
+    return context;
   }
 
   private _serviceManager: IServiceManager = null;
-  private _contextManager: ContextManager = null;
   private _widgetManager: DocumentWidgetManager = null;
   private _registry: IDocumentRegistry = null;
+  private _contexts: Vector<Context<IDocumentModel>> = new Vector<Context<IDocumentModel>>();
+  private _opener: IWidgetOpener = null;
 }
 
 

+ 4 - 4
src/docmanager/savehandler.ts

@@ -34,7 +34,7 @@ class SaveHandler implements IDisposable {
    * Construct a new save handler.
    */
   constructor(options: SaveHandler.IOptions) {
-    this._services = options.services;
+    this._manager = options.manager;
     this._context = options.context;
     this._minInterval = options.saveInterval || 120;
     this._interval = this._minInterval;
@@ -110,7 +110,7 @@ class SaveHandler implements IDisposable {
     }
 
     // Make sure the file has not changed on disk.
-    this._services.contents.get(context.path).then(model => {
+    this._manager.contents.get(context.path).then(model => {
       if (model.last_modified !== context.contentsModel.last_modified) {
         return this._timeConflict(model.last_modified);
       }
@@ -163,7 +163,7 @@ class SaveHandler implements IDisposable {
   private _minInterval = -1;
   private _interval = -1;
   private _context: IDocumentContext<IDocumentModel> = null;
-  private _services: IServiceManager = null;
+  private _manager: IServiceManager = null;
   private _stopped = false;
 }
 
@@ -186,7 +186,7 @@ namespace SaveHandler {
     /**
      * The service manager to use for checking last saved.
      */
-    services: IServiceManager;
+    manager: IServiceManager;
 
     /**
      * The minimum save interval in seconds (default is two minutes).

+ 57 - 74
src/docmanager/widgetmanager.ts

@@ -5,6 +5,18 @@ import {
   IKernel
 } from 'jupyter-js-services';
 
+import {
+  each
+} from 'phosphor/lib/algorithm/iteration';
+
+import {
+  find
+} from 'phosphor/lib/algorithm/searching';
+
+import {
+  Vector
+} from 'phosphor/lib/collections/vector';
+
 import {
   DisposableSet, IDisposable
 } from 'phosphor/lib/core/disposable';
@@ -37,10 +49,6 @@ import {
   IDocumentRegistry, IDocumentContext, IDocumentModel
 } from '../docregistry';
 
-import {
-  ContextManager
-} from './context';
-
 
 /**
  * The class name added to document widgets.
@@ -57,7 +65,6 @@ class DocumentWidgetManager implements IDisposable {
    * Construct a new document widget manager.
    */
   constructor(options: DocumentWidgetManager.IOptions) {
-    this._contextManager = options.contextManager;
     this._registry = options.registry;
   }
 
@@ -76,21 +83,14 @@ class DocumentWidgetManager implements IDisposable {
       return;
     }
     this._registry = null;
-    this._contextManager = null;
-    for (let id in this._widgets) {
-      for (let widget of this._widgets[id]) {
-        widget.dispose();
-      }
-    }
     clearSignalData(this);
   }
 
   /**
    * Create a widget for a document and handle its lifecycle.
    */
-  createWidget(name: string, id: string, kernel?: IKernel.IModel): Widget {
+  createWidget(name: string, context: IDocumentContext<IDocumentModel>, kernel?: IKernel.IModel): Widget {
     let factory = this._registry.getWidgetFactory(name);
-    let context = this._contextManager.getContext(id);
     let widget = factory.createNew(context, kernel);
     Private.nameProperty.set(widget, name);
 
@@ -102,7 +102,7 @@ class DocumentWidgetManager implements IDisposable {
     widget.disposed.connect(() => {
       disposables.dispose();
     });
-    this.adoptWidget(id, widget);
+    this.adoptWidget(context, widget);
     this.setCaption(widget);
     context.contentsModelChanged.connect(() => {
       this.setCaption(widget);
@@ -117,54 +117,47 @@ class DocumentWidgetManager implements IDisposable {
    * Install the message hook for the widget and add to list
    * of known widgets.
    */
-  adoptWidget(id: string, widget: Widget): void {
-    if (!(id in this._widgets)) {
-      this._widgets[id] = [];
-    }
-    this._widgets[id].push(widget);
+  adoptWidget(context: IDocumentContext<IDocumentModel>, widget: Widget): void {
+    let widgets = Private.widgetsProperty.get(context);
+    widgets.pushBack(widget);
     installMessageHook(widget, (handler: IMessageHandler, msg: Message) => {
       return this.filterMessage(handler, msg);
     });
     widget.addClass(DOCUMENT_CLASS);
     widget.title.closable = true;
     widget.disposed.connect(() => {
-      // Remove the widget from the widget registry.
-      let index = this._widgets[id].indexOf(widget);
-      this._widgets[id].splice(index, 1);
+      // Remove the widget.
+      widgets.remove(widget);
       // Dispose of the context if this is the last widget using it.
-      if (!this._widgets[id].length) {
-        let context = this._contextManager.getContext(id);
+      if (!widgets.length) {
         context.dispose();
       }
     });
-    Private.idProperty.set(widget, id);
+    Private.contextProperty.set(widget, context);
   }
 
   /**
-   * See if a widget already exists for the given path and widget name.
+   * See if a widget already exists for the given context and widget name.
    *
    * #### Notes
    * This can be used to use an existing widget instead of opening
    * a new widget.
    */
-  findWidget(path: string, widgetName: string): Widget {
-    let ids = this._contextManager.getIdsForPath(path);
-    for (let id of ids) {
-      for (let widget of this._widgets[id]) {
-        let name = Private.nameProperty.get(widget);
-        if (name === widgetName) {
-          return widget;
-        }
+  findWidget(context: IDocumentContext<IDocumentModel>, widgetName: string): Widget {
+    let widgets = Private.widgetsProperty.get(context);
+    return find(widgets, widget => {
+      let name = Private.nameProperty.get(widget);
+      if (name === widgetName) {
+        return true;
       }
-    }
+    });
   }
 
   /**
    * Get the document context for a widget.
    */
   contextForWidget(widget: Widget): IDocumentContext<IDocumentModel> {
-    let id = Private.idProperty.get(widget);
-    return this._contextManager.getContext(id);
+    return Private.contextProperty.get(widget);
   }
 
   /**
@@ -175,35 +168,21 @@ class DocumentWidgetManager implements IDisposable {
    * as this widget.
    */
   clone(widget: Widget): Widget {
-    let id = Private.idProperty.get(widget);
+    let context = Private.contextProperty.get(widget);
     let name = Private.nameProperty.get(widget);
-    let newWidget = this.createWidget(name, id);
-    this.adoptWidget(id, newWidget);
+    let newWidget = this.createWidget(name, context);
+    this.adoptWidget(context, newWidget);
     return widget;
   }
 
   /**
-   * Close the widgets associated with a given path.
+   * Get the widgets associated with a given context.
    */
-  closeFile(path: string): void {
-    let ids = this._contextManager.getIdsForPath(path);
-    for (let id of ids) {
-      let widgets: Widget[] = 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();
-      }
-    }
+  close(context: IDocumentContext<IDocumentModel>): void {
+    let widgets = Private.widgetsProperty.get(context);
+    each(widgets, widget => {
+      widget.close();
+    });
   }
 
   /**
@@ -231,7 +210,7 @@ class DocumentWidgetManager implements IDisposable {
    * Set the caption for widget title.
    */
   protected setCaption(widget: Widget): void {
-    let context = this.contextForWidget(widget);
+    let context = Private.contextProperty.get(widget);
     let model = context.contentsModel;
     if (!model) {
       widget.title.caption = '';
@@ -272,9 +251,9 @@ class DocumentWidgetManager implements IDisposable {
    */
   private _maybeClose(widget: Widget): Promise<boolean> {
     // Bail if the model is not dirty or other widgets are using the model.
-    let id = Private.idProperty.get(widget);
-    let widgets = this._widgets[id];
-    let model = this._contextManager.getModel(id);
+    let context = Private.contextProperty.get(widget);
+    let widgets = Private.widgetsProperty.get(context);
+    let model = context.model;
     if (!model.dirty || widgets.length > 1) {
       return Promise.resolve(true);
     }
@@ -291,9 +270,7 @@ class DocumentWidgetManager implements IDisposable {
   }
 
   private _closeGuard = false;
-  private _contextManager: ContextManager = null;
   private _registry: IDocumentRegistry = null;
-  private _widgets: { [key: string]: Widget[] } = Object.create(null);
 }
 
 
@@ -311,11 +288,6 @@ namespace DocumentWidgetManager {
      * A document registry instance.
      */
     registry: IDocumentRegistry;
-
-    /**
-     * A context manager instance.
-     */
-    contextManager: ContextManager;
   }
 }
 
@@ -325,11 +297,11 @@ namespace DocumentWidgetManager {
  */
 namespace Private {
   /**
-   * A private attached property for a widget context id.
+   * A private attached property for a widget context.
    */
   export
-  const idProperty = new AttachedProperty<Widget, string>({
-    name: 'id'
+  const contextProperty = new AttachedProperty<Widget, IDocumentContext<IDocumentModel>>({
+    name: 'context'
   });
 
   /**
@@ -339,4 +311,15 @@ namespace Private {
   const nameProperty = new AttachedProperty<Widget, string>({
     name: 'name'
   });
+
+  /**
+   * A private attached property for the widgets associated with a context.
+   */
+  export
+  const widgetsProperty = new AttachedProperty<IDocumentContext<IDocumentModel>, Vector<Widget>>({
+    name: 'widgets',
+    create: () => {
+      return new Vector<Widget>();
+    }
+  });
 }

+ 1 - 1
src/docregistry/default.ts

@@ -169,7 +169,7 @@ defineSignal(DocumentModel.prototype, 'stateChanged');
  * An implementation of a model factory for text files.
  */
 export
-class TextModelFactory implements IModelFactory {
+class TextModelFactory implements IModelFactory<IDocumentModel> {
   /**
    * The name of the model type.
    *

+ 2 - 10
src/docregistry/interfaces.ts

@@ -125,14 +125,6 @@ interface IDocumentContext<T extends IDocumentModel> extends IDisposable {
    */
   disposed: ISignal<IDocumentContext<T>, void>;
 
-  /**
-   * The unique id of the context.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  id: string;
-
   /**
    * Get the model associated with the document.
    *
@@ -355,7 +347,7 @@ interface IWidgetExtension<T extends Widget, U extends IDocumentModel> {
  * The interface for a model factory.
  */
 export
-interface IModelFactory extends IDisposable {
+interface IModelFactory<T extends IDocumentModel> extends IDisposable {
   /**
    * The name of the model.
    *
@@ -386,7 +378,7 @@ interface IModelFactory extends IDisposable {
    *
    * @returns A new document model.
    */
-  createNew(languagePreference?: string): IDocumentModel;
+  createNew(languagePreference?: string): T;
 
   /**
    * Get the preferred kernel language given an extension.

+ 5 - 5
src/docregistry/registry.ts

@@ -65,7 +65,7 @@ interface IDocumentRegistry extends IDisposable {
    * the given factory is already registered, a warning will be logged
    * and this will be a no-op.
    */
-  addModelFactory(factory: IModelFactory): IDisposable;
+  addModelFactory(factory: IModelFactory<IDocumentModel>): IDisposable;
 
   /**
    * Add a widget extension to the registry.
@@ -190,7 +190,7 @@ interface IDocumentRegistry extends IDisposable {
    *
    * @returns A model factory instance.
    */
-  getModelFactoryFor(widgetName: string): IModelFactory;
+  getModelFactoryFor(widgetName: string): IModelFactory<IDocumentModel>;
 
   /**
    * Get a widget factory by name.
@@ -317,7 +317,7 @@ class DocumentRegistry implements IDocumentRegistry {
    * the given factory is already registered, a warning will be logged
    * and this will be a no-op.
    */
-  addModelFactory(factory: IModelFactory): IDisposable {
+  addModelFactory(factory: IModelFactory<IDocumentModel>): IDisposable {
     let name = factory.name.toLowerCase();
     if (this._modelFactories[name]) {
       console.warn(`Duplicate registered factory ${name}`);
@@ -585,7 +585,7 @@ class DocumentRegistry implements IDocumentRegistry {
    *
    * @returns A model factory instance.
    */
-  getModelFactoryFor(widgetName: string): IModelFactory {
+  getModelFactoryFor(widgetName: string): IModelFactory<IDocumentModel> {
     widgetName = widgetName.toLowerCase();
     let wFactoryEx = this._getWidgetFactoryEx(widgetName);
     if (!wFactoryEx) {
@@ -636,7 +636,7 @@ class DocumentRegistry implements IDocumentRegistry {
     return options;
   }
 
-  private _modelFactories: { [key: string]: IModelFactory } = Object.create(null);
+  private _modelFactories: { [key: string]: IModelFactory<IDocumentModel> } = Object.create(null);
   private _widgetFactories: { [key: string]: Private.IWidgetFactoryRecord } = Object.create(null);
   private _defaultWidgetFactory = '';
   private _defaultWidgetFactories: { [key: string]: string } = Object.create(null);

+ 1 - 1
src/notebook/notebook/modelfactory.ts

@@ -18,7 +18,7 @@ import {
  * A model factory for notebooks.
  */
 export
-class NotebookModelFactory implements IModelFactory {
+class NotebookModelFactory implements IModelFactory<INotebookModel> {
   /**
    * The name of the model.
    *