浏览代码

Merge pull request #1085 from blink1073/observable-vector

Clean up the observablelist
Afshin Darian 8 年之前
父节点
当前提交
8e7b5f2b92

+ 0 - 704
src/common/observablelist.ts

@@ -1,704 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  toArray
-} from 'phosphor/lib/algorithm/iteration';
-
-import {
-  move
-} from 'phosphor/lib/algorithm/mutation';
-
-import {
-  indexOf
-} from 'phosphor/lib/algorithm/searching';
-
-import {
-  Vector
-} from 'phosphor/lib/collections/vector';
-
-import {
-  defineSignal, ISignal
-} from 'phosphor/lib/core/signaling';
-
-
-/**
- * The change types which occur on an observable list.
- */
-export
-type ListChangeType =
-  /**
-   * An item was added to the list.
-   */
-  'add' |
-
-  /**
-   * An item was moved in the list.
-   */
-  'move' |
-
-  /**
-   * An item was removed from the list.
-   */
-  'remove' |
-
-  /**
-   * Items were replaced in the list.
-   */
-  'replace' |
-
-  /**
-   * An item was set in the list.
-   */
-  'set';
-
-
-/**
- * The changed args object which is emitted by an observable list.
- */
-export
-interface IListChangedArgs<T> {
-  /**
-   * The type of change undergone by the list.
-   */
-  type: ListChangeType;
-
-  /**
-   * The new index associated with the change.
-   *
-   * The semantics of this value depend upon the change type:
-   *   - `Add`: The index of the added item.
-   *   - `Move`: The new index of the item.
-   *   - `Remove`: Always `-1`.
-   *   - `Replace`: The index of the replacement.
-   *   - `Set`: The index of the set item.
-   */
-  newIndex: number;
-
-  /**
-   * The new value associated with the change.
-   *
-   * The semantics of this value depend upon the change type:
-   *   - `Add`: The item which was added.
-   *   - `Move`: The item which was moved.
-   *   - `Remove`: Always `undefined`.
-   *   - `Replace`: The `items[]` which were added.
-   *   - `Set`: The new item at the index.
-   */
-  newValue: T | T[];
-
-  /**
-   * The old index associated with the change.
-   *
-   * The semantics of this value depend upon the change type:
-   *   - `Add`: Always `-1`.
-   *   - `Move`: The old index of the item.
-   *   - `Remove`: The index of the removed item.
-   *   - `Replace`: The index of the replacement.
-   *   - `Set`: The index of the set item.
-   */
-  oldIndex: number;
-
-  /**
-   * The old value associated with the change.
-   *
-   * The semantics of this value depend upon the change type:
-   *   - `Add`: Always `undefined`.
-   *   - `Move`: The item which was moved.
-   *   - `Remove`: The item which was removed.
-   *   - `Replace`: The `items[]` which were removed.
-   *   - `Set`: The old item at the index.
-   */
-  oldValue: T | T[];
-}
-
-
-/**
- * A sequence container which can be observed for changes.
- */
-export
-interface IObservableList<T> {
-  /**
-   * A signal emitted when the list has changed.
-   */
-  changed: ISignal<IObservableList<T>, IListChangedArgs<T>>;
-
-  /**
-   * The number of items in the list.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  length: number;
-
-  /**
-   * Get the item at a specific index in the list.
-   *
-   * @param index - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns The item at the specified index, or `undefined` if the
-   *   index is out of range.
-   */
-  get(index: number): T;
-
-  /**
-   * Test whether the list contains a specific item.
-   *
-   * @param item - The item of interest.
-   *
-   * @returns `true` if the list contains the item, `false` otherwise.
-   */
-  contains(item: T): boolean;
-
-  /**
-   * Get the index of the first occurence of an item in the list.
-   *
-   * @param item - The item of interest.
-   *
-   * @returns The index of the specified item or `-1` if the item is
-   *   not contained in the list.
-   */
-  indexOf(item: T): number;
-
-  /**
-   * Get a shallow copy of a portion of the list.
-   *
-   * @param start - The start index of the slice, inclusive. If this is
-   *   negative, it is offset from the end of the list. If this is not
-   *   provided, it defaults to `0`. In all cases, it is clamped to the
-   *   bounds of the list.
-   *
-   * @param end - The end index of the slice, exclusive. If this is
-   *   negative, it is offset from the end of the list. If this is not
-   *   provided, it defaults to `length`. In all cases, it is clamped
-   *   to the bounds of the list.
-   *
-   * @returns A new array containing the specified range of items.
-   */
-  slice(start?: number, end?: number): T[];
-
-  /**
-   * Set the item at a specific index.
-   *
-   * @param index - The index of interest. If this is negative, it is
-   *   offset from the end of the list.
-   *
-   * @param item - The item to set at the index.
-   *
-   * @returns The item which occupied the index, or `undefined` if the
-   *   index is out of range.
-   */
-  set(index: number, item: T): T;
-
-  /**
-   * Replace the contents of the list with the specified items.
-   *
-   * @param items - The items to assign to the list.
-   *
-   * @returns An array of the previous list items.
-   *
-   * #### Notes
-   * This is equivalent to `list.replace(0, list.length, items)`.
-   */
-  assign(items: T[]): T[];
-
-  /**
-   * Add an item to the end of the list.
-   *
-   * @param item - The item to add to the list.
-   *
-   * @returns The index at which the item was added.
-   */
-  add(item: T): number;
-
-  /**
-   * Insert an item into the list at a specific index.
-   *
-   * @param index - The index at which to insert the item. If this is
-   *   negative, it is offset from the end of the list. In all cases,
-   *   it is clamped to the bounds of the list.
-   *
-   * @param item - The item to insert into the list.
-   *
-   * @returns The index at which the item was inserted.
-   */
-  insert(index: number, item: T): number;
-
-  /**
-   * Move an item from one index to another.
-   *
-   * @param fromIndex - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @param toIndex - The desired index for the item. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns `true` if the item was moved, `false` otherwise.
-   */
-  move(fromIndex: number, toIndex: number): boolean;
-
-  /**
-   * Remove the first occurrence of a specific item from the list.
-   *
-   * @param item - The item to remove from the list.
-   *
-   * @return The index occupied by the item, or `-1` if the item is
-   *   not contained in the list.
-   */
-  remove(item: T): number;
-
-  /**
-   * Remove the item at a specific index.
-   *
-   * @param index - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns The item at the specified index, or `undefined` if the
-   *   index is out of range.
-   */
-  removeAt(index: number): T;
-
-  /**
-   * Replace items at a specific location in the list.
-   *
-   * @param index - The index at which to modify the list. If this is
-   *   negative, it is offset from the end of the list. In all cases,
-   *   it is clamped to the bounds of the list.
-   *
-   * @param count - The number of items to remove at the given index.
-   *   This is clamped to the length of the list.
-   *
-   * @param items - The items to insert at the specified index.
-   *
-   * @returns An array of the items removed from the list.
-   */
-  replace(index: number, count: number, items: T[]): T[];
-
-  /**
-   * Remove all items from the list.
-   *
-   * @returns An array of the items removed from the list.
-   *
-   * #### Notes
-   * This is equivalent to `list.replace(0, list.length, [])`.
-   */
-  clear(): T[];
-}
-
-
-/**
- * A concrete implementation of [[IObservableList]].
- */
-export
-class ObservableList<T> implements IObservableList<T> {
-  /**
-   * Construct a new observable list.
-   *
-   * @param items - The initial items for the list.
-   */
-  constructor(items?: T[]) {
-    this.internal = new Vector<T>(items);
-  }
-
-  /**
-   * A signal emitted when the list has changed.
-   *
-   * #### Notes
-   * This is a pure delegate to the [[changedSignal]].
-   */
-  changed: ISignal<ObservableList<T>, IListChangedArgs<T>>;
-
-  /**
-   * The number of items in the list.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get length(): number {
-    return this.internal.length;
-  }
-
-  /**
-   * Get the item at a specific index in the list.
-   *
-   * @param index - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns The item at the specified index, or `undefined` if the
-   *   index is out of range.
-   */
-  get(index: number): T {
-    return this.internal.at(this._norm(index));
-  }
-
-  /**
-   * Test whether the list contains a specific item.
-   *
-   * @param item - The item of interest.
-   *
-   * @returns `true` if the list contains the item, `false` otherwise.
-   */
-  contains(item: T): boolean {
-    return indexOf(this.internal, item) !== -1;
-  }
-
-  /**
-   * Get the index of the first occurence of an item in the list.
-   *
-   * @param item - The item of interest.
-   *
-   * @returns The index of the specified item or `-1` if the item is
-   *   not contained in the list.
-   */
-  indexOf(item: T): number {
-    return indexOf(this.internal, item);
-  }
-
-  /**
-   * Get a shallow copy of a portion of the list.
-   *
-   * @param start - The start index of the slice, inclusive. If this is
-   *   negative, it is offset from the end of the list. If this is not
-   *   provided, it defaults to `0`. In all cases, it is clamped to the
-   *   bounds of the list.
-   *
-   * @param end - The end index of the slice, exclusive. If this is
-   *   negative, it is offset from the end of the list. If this is not
-   *   provided, it defaults to `length`. In all cases, it is clamped
-   *   to the bounds of the list.
-   *
-   * @returns A new array containing the specified range of items.
-   */
-  slice(start?: number, end?: number): T[] {
-    return toArray(this.internal).slice(start, end);
-  }
-
-  /**
-   * Set the item at a specific index.
-   *
-   * @param index - The index of interest. If this is negative, it is
-   *   offset from the end of the list.
-   *
-   * @param item - The item to set at the index.
-   *
-   * @returns The item which occupied the index, or `undefined` if the
-   *   index is out of range.
-   */
-  set(index: number, item: T): T {
-    let i = this._norm(index);
-    if (!this._check(i)) {
-      return void 0;
-    }
-    return this.setItem(i, item);
-  }
-
-  /**
-   * Replace the contents of the list with the specified items.
-   *
-   * @param items - The items to assign to the list.
-   *
-   * @returns An array of the previous list items.
-   *
-   * #### Notes
-   * This is equivalent to `list.replace(0, list.length, items)`.
-   */
-  assign(items: T[]): T[] {
-    return this.replaceItems(0, this.internal.length, items);
-  }
-
-  /**
-   * Add an item to the end of the list.
-   *
-   * @param item - The item to add to the list.
-   *
-   * @returns The index at which the item was added.
-   */
-  add(item: T): number {
-    return this.addItem(this.internal.length, item);
-  }
-
-  /**
-   * Insert an item into the list at a specific index.
-   *
-   * @param index - The index at which to insert the item. If this is
-   *   negative, it is offset from the end of the list. In all cases,
-   *   it is clamped to the bounds of the list.
-   *
-   * @param item - The item to insert into the list.
-   *
-   * @returns The index at which the item was inserted.
-   */
-  insert(index: number, item: T): number {
-    return this.addItem(this._clamp(index), item);
-  }
-
-  /**
-   * Move an item from one index to another.
-   *
-   * @param fromIndex - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @param toIndex - The desired index for the item. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns `true` if the item was moved, `false` otherwise.
-   */
-  move(fromIndex: number, toIndex: number): boolean {
-    let i = this._norm(fromIndex);
-    if (!this._check(i)) {
-      return false;
-    }
-    let j = this._norm(toIndex);
-    if (!this._check(j)) {
-      return false;
-    }
-    return this.moveItem(i, j);
-  }
-
-  /**
-   * Remove the first occurrence of a specific item from the list.
-   *
-   * @param item - The item to remove from the list.
-   *
-   * @return The index occupied by the item, or `-1` if the item is
-   *   not contained in the list.
-   */
-  remove(item: T): number {
-    let i = indexOf(this.internal, item);
-    if (i !== -1) {
-      this.removeItem(i);
-    }
-    return i;
-  }
-
-  /**
-   * Remove the item at a specific index.
-   *
-   * @param index - The index of the item of interest. If this is
-   *   negative, it is offset from the end of the list.
-   *
-   * @returns The item at the specified index, or `undefined` if the
-   *   index is out of range.
-   */
-  removeAt(index: number): T {
-    let i = this._norm(index);
-    if (!this._check(i)) {
-      return void 0;
-    }
-    return this.removeItem(i);
-  }
-
-  /**
-   * Replace items at a specific location in the list.
-   *
-   * @param index - The index at which to modify the list. If this is
-   *   negative, it is offset from the end of the list. In all cases,
-   *   it is clamped to the bounds of the list.
-   *
-   * @param count - The number of items to remove at the given index.
-   *   This is clamped to the length of the list.
-   *
-   * @param items - The items to insert at the specified index.
-   *
-   * @returns An array of the items removed from the list.
-   */
-  replace(index: number, count: number, items: T[]): T[] {
-    return this.replaceItems(this._norm(index), this._limit(count), items);
-  }
-
-  /**
-   * Remove all items from the list.
-   *
-   * @returns An array of the items removed from the list.
-   *
-   * #### Notes
-   * This is equivalent to `list.replace(0, list.length, [])`.
-   */
-  clear(): T[] {
-    return this.replaceItems(0, this.internal.length, []);
-  }
-
-  /**
-   * The protected internal array of items for the list.
-   *
-   * #### Notes
-   * Subclasses may access this array directly as needed.
-   */
-  protected internal: Vector<T>;
-
-  /**
-   * Add an item to the list at the specified index.
-   *
-   * @param index - The index at which to add the item. This must be
-   *   an integer in the range `[0, internal.length]`.
-   *
-   * @param item - The item to add at the specified index.
-   *
-   * @returns The index at which the item was added.
-   *
-   * #### Notes
-   * This may be reimplemented by subclasses to customize the behavior.
-   */
-  protected addItem(index: number, item: T): number {
-    this.internal.insert(index, item);
-    this.changed.emit({
-      type: 'add',
-      newIndex: index,
-      newValue: item,
-      oldIndex: -1,
-      oldValue: void 0,
-    });
-    return index;
-  }
-
-  /**
-   * Move an item in the list from one index to another.
-   *
-   * @param fromIndex - The initial index of the item. This must be
-   *   an integer in the range `[0, internal.length)`.
-   *
-   * @param toIndex - The desired index for the item. This must be
-   *   an integer in the range `[0, internal.length)`.
-   *
-   * @returns `true` if the item was moved, `false` otherwise.
-   *
-   * #### Notes
-   * This may be reimplemented by subclasses to customize the behavior.
-   */
-  protected moveItem(fromIndex: number, toIndex: number): boolean {
-    let before = this.internal.at(toIndex);
-    move(this.internal, fromIndex, toIndex);
-    let after = this.internal.at(toIndex);
-    if (before === after) {
-      return;
-    }
-    this.changed.emit({
-      type: 'move',
-      newIndex: toIndex,
-      newValue: after,
-      oldIndex: fromIndex,
-      oldValue: after,
-    });
-    return true;
-  }
-
-  /**
-   * Remove the item from the list at the specified index.
-   *
-   * @param index - The index of the item to remove. This must be
-   *   an integer in the range `[0, internal.length)`.
-   *
-   * @returns The item removed from the list.
-   *
-   * #### Notes
-   * This may be reimplemented by subclasses to customize the behavior.
-   */
-  protected removeItem(index: number): T {
-    let item = this.internal.removeAt(index);
-    this.changed.emit({
-      type: 'remove',
-      newIndex: -1,
-      newValue: void 0,
-      oldIndex: index,
-      oldValue: item,
-    });
-    return item;
-  }
-
-  /**
-   * Replace items at a specific location in the list.
-   *
-   * @param index - The index at which to modify the list. This must
-   *   be an integer in the range `[0, internal.length]`.
-   *
-   * @param count - The number of items to remove from the list. This
-   *   must be an integer in the range `[0, internal.length]`.
-   *
-   * @param items - The items to insert at the specified index.
-   *
-   * @returns An array of the items removed from the list.
-   *
-   * #### Notes
-   * This may be reimplemented by subclasses to customize the behavior.
-   */
-  protected replaceItems(index: number, count: number, items: T[]): T[] {
-    let old: T[] = [];
-    while (count-- > 0) {
-      old.push(this.internal.removeAt(index));
-    }
-
-    let i = index;
-    let j = 0;
-    let len = items.length;
-    while (j < len) {
-      this.internal.insert(i++, items[j++]);
-    }
-    this.changed.emit({
-      type: 'replace',
-      newIndex: index,
-      newValue: items,
-      oldIndex: index,
-      oldValue: old,
-    });
-    return old;
-  }
-
-  /**
-   * Set the item at a specific index in the list.
-   *
-   * @param index - The index of interest. This must be an integer in
-   *   the range `[0, internal.length)`.
-   *
-   * @param item - The item to set at the index.
-   *
-   * @returns The item which previously occupied the specified index.
-   *
-   * #### Notes
-   * This may be reimplemented by subclasses to customize the behavior.
-   */
-  protected setItem(index: number, item: T): T {
-    let old = this.internal.at(index);
-    this.internal.set(index, item);
-    this.changed.emit({
-      type: 'set',
-      newIndex: index,
-      newValue: item,
-      oldIndex: index,
-      oldValue: old,
-    });
-    return old;
-  }
-
-  /**
-   * Normalize an index and offset negative values from the list end.
-   */
-  private _norm(i: number): number {
-    return i < 0 ? Math.floor(i) + this.internal.length : Math.floor(i);
-  }
-
-  /**
-   * Check whether a normalized index is in range.
-   */
-  private _check(i: number): boolean {
-    return i >= 0 && i < this.internal.length;
-  }
-
-  /**
-   * Normalize and clamp an index to the list bounds.
-   */
-  private _clamp(i: number): number {
-    return Math.max(0, Math.min(this._norm(i), this.internal.length));
-  }
-
-  /**
-   * Normalize and limit a count to the length of the list.
-   */
-  private _limit(c: number): number {
-    return Math.max(0, Math.min(Math.floor(c), this.internal.length));
-  }
-}
-
-
-// Define the signals for the `ObservableList` class.
-defineSignal(ObservableList.prototype, 'changed');

+ 693 - 0
src/common/observablevector.ts

@@ -0,0 +1,693 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IterableOrArrayLike, each
+} from 'phosphor/lib/algorithm/iteration';
+
+import {
+  indexOf
+} from 'phosphor/lib/algorithm/searching';
+
+import {
+  ISequence
+} from 'phosphor/lib/algorithm/sequence';
+
+import {
+  Vector
+} from 'phosphor/lib/collections/vector';
+
+import {
+  IDisposable
+} from 'phosphor/lib/core/disposable';
+
+import {
+  clearSignalData, defineSignal, ISignal
+} from 'phosphor/lib/core/signaling';
+
+
+/**
+ * A vector which can be observed for changes.
+ */
+export
+interface IObservableVector<T> extends IDisposable, ISequence<T> {
+  /**
+   * A signal emitted when the vector has changed.
+   */
+  changed: ISignal<IObservableVector<T>, ObservableVector.IChangedArgs<T>>;
+
+  /**
+   * Test whether the vector is empty.
+   *
+   * @returns `true` if the vector is empty, `false` otherwise.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  readonly isEmpty: boolean;
+
+  /**
+   * Get the value at the front of the vector.
+   *
+   * @returns The value at the front of the vector, or `undefined` if
+   *   the vector is empty.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  readonly front: T;
+
+  /**
+   * Get the value at the back of the vector.
+   *
+   * @returns The value at the back of the vector, or `undefined` if
+   *   the vector is empty.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  readonly back: T;
+
+  /**
+   * Set the value at the specified index.
+   *
+   * @param index - The positive integer index of interest.
+   *
+   * @param value - The value to set at the specified index.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral or out of range.
+   */
+  set(index: number, value: T): void;
+
+  /**
+   * Add a value to the back of the vector.
+   *
+   * @param value - The value to add to the back of the vector.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  pushBack(value: T): number;
+
+  /**
+   * Remove and return the value at the back of the vector.
+   *
+   * @returns The value at the back of the vector, or `undefined` if
+   *   the vector is empty.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value are invalidated.
+   */
+  popBack(): T;
+
+  /**
+   * Insert a value into the vector at a specific index.
+   *
+   * @param index - The index at which to insert the value.
+   *
+   * @param value - The value to set at the specified index.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Notes
+   * The `index` will be clamped to the bounds of the vector.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral.
+   */
+  insert(index: number, value: T): number;
+
+  /**
+   * Remove the first occurrence of a value from the vector.
+   *
+   * @param value - The value of interest.
+   *
+   * @returns The index of the removed value, or `-1` if the value
+   *   is not contained in the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value and beyond are invalidated.
+   *
+   * #### Notes
+   * Comparison is performed using strict `===` equality.
+   */
+  remove(value: T): number;
+
+  /**
+   * Remove and return the value at a specific index.
+   *
+   * @param index - The index of the value of interest.
+   *
+   * @returns The value at the specified index, or `undefined` if the
+   *   index is out of range.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value and beyond are invalidated.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral.
+   */
+  removeAt(index: number): T;
+
+  /**
+   * Remove all values from the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * All current iterators are invalidated.
+   */
+  clear(): void;
+
+  /**
+   * Move a value from one index to another.
+   *
+   * @parm fromIndex - The index of the element to move.
+   *
+   * @param toIndex - The index to move the element to.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the lesser of the `fromIndex` and the `toIndex`
+   * and beyond are invalidated.
+   *
+   * #### Undefined Behavior
+   * A `fromIndex` or a `toIndex` which is non-integral.
+   */
+  move(fromIndex: number, toIndex: number): void;
+
+  /**
+   * Push a set of values to the back of the vector.
+   *
+   * @param values - An iterable or array-like set of values to add.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  pushAll(values: IterableOrArrayLike<T>): number;
+
+  /**
+   * Insert a set of items into the vector at the specified index.
+   *
+   * @param index - The index at which to insert the values.
+   *
+   * @param values - The values to insert at the specified index.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity.
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Notes
+   * The `index` will be clamped to the bounds of the vector.
+   *
+   * #### Undefined Behavior.
+   * An `index` which is non-integral.
+   */
+  insertAll(index: number, values: IterableOrArrayLike<T>): number;
+
+  /**
+   * Remove a range of items from the list.
+   *
+   * @param startIndex - The start index of the range to remove (inclusive).
+   *
+   * @param endIndex - The end index of the range to remove (exclusive).
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * Iterators pointing to the first removed value and beyond are invalid.
+   *
+   * #### Undefined Behavior
+   * A `startIndex` or `endIndex` which is non-integral.
+   */
+  removeRange(startIndex: number, endIndex: number): number;
+}
+
+
+/**
+ * A concrete implementation of [[IObservableVector]].
+ */
+export
+class ObservableVector<T> extends Vector<T> implements IObservableVector<T> {
+  /**
+   * A signal emitted when the list has changed.
+   */
+  changed: ISignal<ObservableVector<T>, ObservableVector.IChangedArgs<T>>;
+
+  /**
+   * Test whether the list has been disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Dispose of the resources held by the list.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+    this.clear();
+    clearSignalData(this);
+  }
+
+  /**
+   * Set the value at the specified index.
+   *
+   * @param index - The positive integer index of interest.
+   *
+   * @param value - The value to set at the specified index.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral or out of range.
+   */
+  set(index: number, value: T): void {
+    let oldValues = new Vector([this.at(index)]);
+    super.set(index, value);
+    this.changed.emit({
+      type: 'set',
+      oldIndex: index,
+      newIndex: index,
+      oldValues,
+      newValues: new Vector([value])
+    });
+  }
+
+  /**
+   * Add a value to the back of the vector.
+   *
+   * @param value - The value to add to the back of the vector.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  pushBack(value: T): number {
+    let num = super.pushBack(value);
+    this.changed.emit({
+      type: 'add',
+      oldIndex: -1,
+      newIndex: this.length - 1,
+      oldValues: this._emptySequence,
+      newValues: new Vector([value])
+    });
+    return num;
+  }
+
+  /**
+   * Remove and return the value at the back of the vector.
+   *
+   * @returns The value at the back of the vector, or `undefined` if
+   *   the vector is empty.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value are invalidated.
+   */
+  popBack(): T {
+    let value = super.popBack();
+    this.changed.emit({
+      type: 'remove',
+      oldIndex: this.length,
+      newIndex: -1,
+      oldValues: new Vector([value]),
+      newValues: this._emptySequence
+    });
+    return value;
+  }
+
+  /**
+   * Insert a value into the vector at a specific index.
+   *
+   * @param index - The index at which to insert the value.
+   *
+   * @param value - The value to set at the specified index.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Notes
+   * The `index` will be clamped to the bounds of the vector.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral.
+   */
+  insert(index: number, value: T): number {
+    let num = super.insert(index, value);
+    this.changed.emit({
+      type: 'add',
+      oldIndex: -1,
+      newIndex: index,
+      oldValues: this._emptySequence,
+      newValues: new Vector([value])
+    });
+    return num;
+  }
+
+  /**
+   * Remove the first occurrence of a value from the vector.
+   *
+   * @param value - The value of interest.
+   *
+   * @returns The index of the removed value, or `-1` if the value
+   *   is not contained in the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value and beyond are invalidated.
+   *
+   * #### Notes
+   * Comparison is performed using strict `===` equality.
+   */
+  remove(value: T): number {
+    let index = indexOf(this, value);
+    this.removeAt(index);
+    return index;
+  }
+
+  /**
+   * Remove and return the value at a specific index.
+   *
+   * @param index - The index of the value of interest.
+   *
+   * @returns The value at the specified index, or `undefined` if the
+   *   index is out of range.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the removed value and beyond are invalidated.
+   *
+   * #### Undefined Behavior
+   * An `index` which is non-integral.
+   */
+  removeAt(index: number): T {
+    let value = super.removeAt(index);
+    this.changed.emit({
+      type: 'remove',
+      oldIndex: index,
+      newIndex: -1,
+      oldValues: new Vector([value]),
+      newValues: this._emptySequence
+    });
+    return value;
+  }
+
+  /**
+   * Remove all values from the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * All current iterators are invalidated.
+   */
+  clear(): void {
+    let oldValues = new Vector(this.iter());
+    super.clear();
+    this.changed.emit({
+      type: 'remove',
+      oldIndex: 0,
+      newIndex: 0,
+      oldValues,
+      newValues: this._emptySequence
+    });
+  }
+
+  /**
+   * Move a value from one index to another.
+   *
+   * @parm fromIndex - The index of the element to move.
+   *
+   * @param toIndex - The index to move the element to.
+   *
+   * #### Complexity
+   * Constant.
+   *
+   * #### Iterator Validity
+   * Iterators pointing at the lesser of the `fromIndex` and the `toIndex`
+   * and beyond are invalidated.
+   *
+   * #### Undefined Behavior
+   * A `fromIndex` or a `toIndex` which is non-integral.
+   */
+  move(fromIndex: number, toIndex: number): void {
+    let value = this.at(fromIndex);
+    super.removeAt(fromIndex);
+    if (toIndex < fromIndex) {
+      super.insert(toIndex - 1, value);
+    } else {
+      super.insert(toIndex, value);
+    }
+    let vector = new Vector([value]);
+    this.changed.emit({
+      type: 'move',
+      oldIndex: fromIndex,
+      newIndex: toIndex,
+      oldValues: vector,
+      newValues: vector
+    });
+  }
+
+  /**
+   * Push a set of values to the back of the vector.
+   *
+   * @param values - An iterable or array-like set of values to add.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   */
+  pushAll(values: IterableOrArrayLike<T>): number {
+    let newIndex = this.length;
+    each(values, value => { super.pushBack(value); });
+    this.changed.emit({
+      type: 'add',
+      oldIndex: -1,
+      newIndex,
+      oldValues: this._emptySequence,
+      newValues: new Vector(values)
+    });
+    return this.length;
+  }
+
+  /**
+   * Insert a set of items into the vector at the specified index.
+   *
+   * @param index - The index at which to insert the values.
+   *
+   * @param values - The values to insert at the specified index.
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity.
+   * Linear.
+   *
+   * #### Iterator Validity
+   * No changes.
+   *
+   * #### Notes
+   * The `index` will be clamped to the bounds of the vector.
+   *
+   * #### Undefined Behavior.
+   * An `index` which is non-integral.
+   */
+  insertAll(index: number, values: IterableOrArrayLike<T>): number {
+    let newIndex = index;
+    each(values, value => { super.insert(index++, value); });
+    this.changed.emit({
+      type: 'add',
+      oldIndex: -1,
+      newIndex,
+      oldValues: this._emptySequence,
+      newValues: new Vector(values)
+    });
+    return this.length;
+  }
+
+  /**
+   * Remove a range of items from the list.
+   *
+   * @param startIndex - The start index of the range to remove (inclusive).
+   *
+   * @param endIndex - The end index of the range to remove (exclusive).
+   *
+   * @returns The new length of the vector.
+   *
+   * #### Complexity
+   * Linear.
+   *
+   * #### Iterator Validity
+   * Iterators pointing to the first removed value and beyond are invalid.
+   *
+   * #### Undefined Behavior
+   * A `startIndex` or `endIndex` which is non-integral.
+   */
+  removeRange(startIndex: number, endIndex: number): number {
+    let oldValues = new Vector<T>();
+    for (let i = startIndex; i < endIndex; i++) {
+      oldValues.pushBack(super.removeAt(startIndex));
+    }
+    this.changed.emit({
+      type: 'remove',
+      oldIndex: startIndex,
+      newIndex: -1,
+      oldValues,
+      newValues: this._emptySequence
+    });
+    return this.length;
+  }
+
+  private _isDisposed = false;
+  private _emptySequence = new Vector<T>();
+}
+
+
+/**
+ * The namespace for `ObservableVector` class statics.
+ */
+export
+namespace ObservableVector {
+  /**
+   * The change types which occur on an observable list.
+   */
+  export
+  type ChangeType =
+    /**
+     * Item(s) were added to the list.
+     */
+    'add' |
+
+    /**
+     * An item was moved within the list.
+     */
+    'move' |
+
+    /**
+     * Item(s) were removed from the list.
+     */
+    'remove' |
+
+    /**
+     * An item was set in the list.
+     */
+    'set';
+
+  /**
+   * The changed args object which is emitted by an observable list.
+   */
+  export
+  interface IChangedArgs<T> {
+    /**
+     * The type of change undergone by the list.
+     */
+    type: ChangeType;
+
+    /**
+     * The new index associated with the change.
+     */
+    newIndex: number;
+
+    /**
+     * The new values associated with the change.
+     *
+     * #### Notes
+     * The values will be contiguous starting at the `newIndex`.
+     */
+    newValues: ISequence<T>;
+
+    /**
+     * The old index associated with the change.
+     */
+    oldIndex: number;
+
+    /**
+     * The old values associated with the change.
+     *
+     * #### Notes
+     * The values will be contiguous starting at the `oldIndex`.
+     */
+    oldValues: ISequence<T>;
+  }
+}
+
+
+// Define the signals for the `ObservableVector` class.
+defineSignal(ObservableVector.prototype, 'changed');

+ 111 - 118
src/notebook/common/undo.ts

@@ -2,16 +2,20 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  IDisposable
-} from 'phosphor/lib/core/disposable';
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
 
 import {
-  clearSignalData
-} from 'phosphor/lib/core/signaling';
+  each
+} from 'phosphor/lib/algorithm/iteration';
 
 import {
-  IObservableList, IListChangedArgs, ObservableList
-} from '../../common/observablelist';
+  Vector
+} from 'phosphor/lib/collections/vector';
+
+import {
+  IObservableVector, ObservableVector
+} from '../../common/observablevector';
 
 
 /**
@@ -22,29 +26,71 @@ interface ISerializable {
   /**
    * Convert the object to JSON.
    */
-  toJSON(): any;
+  toJSON(): JSONObject;
+}
+
+
+/**
+ * An observable vector that supports undo/redo.
+ */
+export
+interface IObservableUndoableVector<T extends ISerializable> extends IObservableVector<T> {
+  /**
+   * Whether the object can redo changes.
+   */
+  readonly canRedo: boolean;
+
+  /**
+   * Whether the object can undo changes.
+   */
+  readonly canUndo: boolean;
+
+  /**
+   * Begin a compound operation.
+   *
+   * @param isUndoAble - Whether the operation is undoable.
+   *   The default is `false`.
+   */
+  beginCompoundOperation(isUndoAble?: boolean): void;
+
+  /**
+   * End a compound operation.
+   */
+  endCompoundOperation(): void;
+
+  /**
+   * Undo an operation.
+   */
+  undo(): void;
+
+  /**
+   * Redo an operation.
+   */
+  redo(): void;
+
+  /**
+   * Clear the change stack.
+   */
+  clearUndo(): void;
 }
 
 
 /**
- * An observable list that supports undo/redo.
+ * A concrete implementation of an observable undoable vector.
  */
 export
-class ObservableUndoableList<T extends ISerializable> extends ObservableList<T> implements IDisposable {
+class ObservableUndoableVector<T extends ISerializable> extends ObservableVector<T> implements IObservableUndoableVector<T> {
   /**
-   * Construct a new undoable observable list.
+   * Construct a new undoable observable vector.
    */
-  constructor(factory: (value: any) => T) {
+  constructor(factory: (value: JSONObject) => T) {
     super();
     this._factory = factory;
-    this.changed.connect(this._onListChanged, this);
+    this.changed.connect(this._onVectorChanged, this);
   }
 
   /**
    * Whether the object can redo changes.
-   *
-   * #### Notes
-   * This is a read-only property.
    */
   get canRedo(): boolean {
     return this._index < this._stack.length - 1;
@@ -52,22 +98,16 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
 
   /**
    * Whether the object can undo changes.
-   *
-   * #### Notes
-   * This is a read-only property.
    */
   get canUndo(): boolean {
     return this._index >= 0;
   }
 
   /**
-   * Get whether the object is disposed.
-   *
-   * #### Notes
-   * This is a read-only property.
+   * Test whether the vector is disposed.
    */
   get isDisposed(): boolean {
-    return this._stack === null;
+    return this._factory === null;
   }
 
   /**
@@ -78,13 +118,16 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
     if (this.isDisposed) {
       return;
     }
-    clearSignalData(this);
     this._factory = null;
     this._stack = null;
+    super.dispose();
   }
 
   /**
    * Begin a compound operation.
+   *
+   * @param isUndoAble - Whether the operation is undoable.
+   *   The default is `true`.
    */
   beginCompoundOperation(isUndoAble?: boolean): void {
     this._inCompound = true;
@@ -144,10 +187,10 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
   }
 
   /**
-   * Handle a change in the list.
+   * Handle a change in the vector.
    */
-  private _onListChanged(list: IObservableList<T>, change: IListChangedArgs<T>): void {
-    if (!this._isUndoable) {
+  private _onVectorChanged(list: IObservableVector<T>, change: ObservableVector.IChangedArgs<T>): void {
+    if (this.isDisposed || !this._isUndoable) {
       return;
     }
     // Clear everything after this position if necessary.
@@ -173,28 +216,30 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
   /**
    * Undo a change event.
    */
-  private _undoChange(change: IListChangedArgs<any>): void {
-    let value: T;
+  private _undoChange(change: ObservableVector.IChangedArgs<JSONObject>): void {
+    let index = 0;
+    let factory = this._factory;
     switch (change.type) {
     case 'add':
-      this.removeAt(change.newIndex);
+      each(change.newValues, () => {
+        this.removeAt(change.newIndex);
+      });
       break;
     case 'set':
-      value = this._createValue(change.oldValue as any);
-      this.set(change.oldIndex, value);
+      index = change.oldIndex;
+      each(change.oldValues, value => {
+        this.set(index++, factory(value));
+      });
       break;
     case 'remove':
-      value = this._createValue(change.oldValue as any);
-      this.insert(change.oldIndex, value);
+      index = change.oldIndex;
+      each(change.oldValues, value => {
+        this.insert(index++, factory(value));
+      });
       break;
     case 'move':
       this.move(change.newIndex, change.oldIndex);
       break;
-    case 'replace':
-      let len = (change.newValue as any[]).length;
-      let values = this._createValues(change.oldValue as any[]);
-      this.replace(change.oldIndex, len, values);
-      break;
     default:
       return;
     }
@@ -203,105 +248,53 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
   /**
    * Redo a change event.
    */
-  private _redoChange(change: IListChangedArgs<any>): void {
-    let value: T;
+  private _redoChange(change: ObservableVector.IChangedArgs<JSONObject>): void {
+    let index = 0;
+    let factory = this._factory;
     switch (change.type) {
     case 'add':
-      value = this._createValue(change.newValue as any);
-      this.insert(change.newIndex, value);
+      index = change.newIndex;
+      each(change.newValues, value => {
+        this.insert(index++, factory(value));
+      });
       break;
     case 'set':
-      value = this._createValue(change.newValue as any);
-      this.set(change.newIndex, value);
+      index = change.newIndex;
+      each(change.newValues, value => {
+        this.set(change.newIndex++, factory(value));
+      });
       break;
     case 'remove':
-      this.removeAt(change.oldIndex);
+      each(change.oldValues, () => {
+        this.removeAt(change.oldIndex);
+      });
       break;
     case 'move':
       this.move(change.oldIndex, change.newIndex);
       break;
-    case 'replace':
-      let len = (change.oldValue as any[]).length;
-      let cells = this._createValues(change.newValue as any[]);
-      this.replace(change.oldIndex, len, cells);
-      break;
     default:
       return;
     }
   }
 
-  /**
-   * Create a value from JSON.
-   */
-  private _createValue(data: any): T {
-    let factory = this._factory;
-    return factory(data);
-  }
-
-  /**
-   * Create a list of cell models from JSON.
-   */
-  private _createValues(bundles: any[]): T[] {
-    let values: T[] = [];
-    for (let bundle of bundles) {
-      values.push(this._createValue(bundle));
-    }
-    return values;
-  }
-
   /**
    * Copy a change as JSON.
    */
-  private _copyChange(change: IListChangedArgs<T>): IListChangedArgs<any> {
-    if (change.type === 'replace') {
-      return this._copyReplace(change);
-    }
-    let oldValue: any = null;
-    let newValue: any = null;
-    switch (change.type) {
-    case 'add':
-    case 'set':
-    case 'remove':
-      if (change.oldValue) {
-        oldValue = (change.oldValue as T).toJSON();
-      }
-      if (change.newValue) {
-        newValue = (change.newValue as T).toJSON();
-      }
-      break;
-    case 'move':
-      // Only need the indices.
-      break;
-    default:
-      return;
-    }
+  private _copyChange(change: ObservableVector.IChangedArgs<T>): ObservableVector.IChangedArgs<JSONObject> {
+    let oldValues = new Vector<JSONObject>();
+    each(change.oldValues, value => {
+      oldValues.pushBack(value.toJSON());
+    });
+    let newValues = new Vector<JSONObject>();
+    each(change.newValues, value => {
+      newValues.pushBack(value.toJSON());
+    });
     return {
       type: change.type,
       oldIndex: change.oldIndex,
       newIndex: change.newIndex,
-      oldValue,
-      newValue
-    };
-  }
-
-  /**
-   * Copy a replace change as JSON.
-   */
-  private _copyReplace(change: IListChangedArgs<T>): IListChangedArgs<any> {
-    let oldValue: any[] = [];
-    for (let value of (change.oldValue as T[])) {
-      oldValue.push(value.toJSON());
-    }
-    let newValue: any[] = [];
-    for (let value of (change.newValue as T[])) {
-      newValue.push(value.toJSON());
-    }
-    return {
-      type: 'replace',
-      oldIndex: change.oldIndex,
-      newIndex: change.newIndex,
-      oldValue,
-      newValue
+      oldValues,
+      newValues
     };
   }
 
@@ -309,6 +302,6 @@ class ObservableUndoableList<T extends ISerializable> extends ObservableList<T>
   private _isUndoable = true;
   private _madeCompoundChange = false;
   private _index = -1;
-  private _stack: IListChangedArgs<any>[][] = [];
-  private _factory: (value: any) => T = null;
+  private _stack: ObservableVector.IChangedArgs<JSONObject>[][] = [];
+  private _factory: (value: JSONObject) => T = null;
 }

+ 31 - 11
src/notebook/notebook/actions.ts

@@ -5,6 +5,14 @@ import {
   IKernel, KernelMessage
 } from '@jupyterlab/services';
 
+import {
+  each
+} from 'phosphor/lib/algorithm/iteration';
+
+import {
+  indexOf
+} from 'phosphor/lib/algorithm/searching';
+
 import {
   MimeData as IClipboard
 } from 'phosphor/lib/core/mimedata';
@@ -80,7 +88,12 @@ namespace NotebookActions {
     clone1.source = orig.slice(position).replace(/^\s+/g, '');
 
     // Make the changes while preserving history.
-    nbModel.cells.replace(index, 1, [clone0, clone1]);
+    let cells = nbModel.cells;
+    cells.beginCompoundOperation();
+    cells.set(index, clone0);
+    cells.insert(index + 1, clone1);
+    cells.endCompoundOperation();
+
     widget.activeCellIndex++;
     widget.scrollToActiveCell();
   }
@@ -129,7 +142,7 @@ namespace NotebookActions {
         return;
       }
       // Otherwise merge with the next cell.
-      let cellModel = cells.get(index + 1);
+      let cellModel = cells.at(index + 1);
       toMerge.push(cellModel.source);
       toDelete.push(cellModel);
     }
@@ -185,7 +198,7 @@ namespace NotebookActions {
       let child = widget.childAt(i);
       if (widget.isSelected(child)) {
         index = i;
-        toDelete.push(cells.get(i));
+        toDelete.push(cells.at(i));
       }
     }
 
@@ -424,7 +437,7 @@ namespace NotebookActions {
     let model = widget.model;
     if (widget.activeCellIndex === widget.childCount() - 1) {
       let cell = model.factory.createCodeCell();
-      model.cells.add(cell);
+      model.cells.pushBack(cell);
       widget.activeCellIndex++;
       widget.mode = 'edit';
     } else {
@@ -670,7 +683,7 @@ namespace NotebookActions {
     // If there are no cells, add a code cell.
     if (!model.cells.length) {
       let cell = model.factory.createCodeCell();
-      model.cells.add(cell);
+      model.cells.pushBack(cell);
     }
     model.cells.endCompoundOperation();
 
@@ -717,7 +730,14 @@ namespace NotebookActions {
       }
     }
     let index = widget.activeCellIndex;
-    widget.model.cells.replace(index + 1, 0, newCells);
+
+    let cells = widget.model.cells;
+    cells.beginCompoundOperation();
+    each(newCells, cell => {
+      cells.insert(++index, cell);
+    });
+    cells.endCompoundOperation();
+
     widget.activeCellIndex += newCells.length;
     Private.deselectCells(widget);
   }
@@ -817,7 +837,7 @@ namespace NotebookActions {
     }
     let cells = widget.model.cells;
     for (let i = 0; i < cells.length; i++) {
-      let cell = cells.get(i) as CodeCellModel;
+      let cell = cells.at(i) as CodeCellModel;
       let child = widget.childAt(i);
       if (widget.isSelected(child) && cell.type === 'code') {
         cell.outputs.clear();
@@ -841,7 +861,7 @@ namespace NotebookActions {
     }
     let cells = widget.model.cells;
     for (let i = 0; i < cells.length; i++) {
-      let cell = cells.get(i) as CodeCellModel;
+      let cell = cells.at(i) as CodeCellModel;
       if (cell.type === 'code') {
         cell.outputs.clear();
         cell.executionCount = null;
@@ -873,7 +893,7 @@ namespace NotebookActions {
     for (let i = 0; i < cells.length; i++) {
       let child = widget.childAt(i) as MarkdownCellWidget;
       if (widget.isSelected(child)) {
-        Private.setMarkdownHeader(cells.get(i), level);
+        Private.setMarkdownHeader(cells.at(i), level);
       }
     }
     changeCellType(widget, 'markdown');
@@ -972,9 +992,9 @@ namespace Private {
     let cell = parent.model.factory.createCodeCell();
     cell.source = text;
     let cells = parent.model.cells;
-    let i = cells.indexOf(child.model);
+    let i = indexOf(cells, child.model);
     if (i === -1) {
-      cells.add(cell);
+      cells.pushBack(cell);
     } else {
       cells.insert(i + 1, cell);
     }

+ 33 - 32
src/notebook/notebook/model.ts

@@ -5,6 +5,10 @@ import {
   utils
 } from '@jupyterlab/services';
 
+import {
+  each
+} from 'phosphor/lib/algorithm/iteration';
+
 import {
   deepEqual
 } from 'phosphor/lib/algorithm/json';
@@ -14,8 +18,8 @@ import {
 } from 'phosphor/lib/core/signaling';
 
 import {
-  IObservableList, IListChangedArgs
-} from '../../common/observablelist';
+  IObservableVector, ObservableVector
+} from '../../common/observablevector';
 
 import {
   DocumentModel, DocumentRegistry
@@ -35,7 +39,7 @@ import {
 } from '../common/metadata';
 
 import {
-  ObservableUndoableList
+  IObservableUndoableVector, ObservableUndoableVector
 } from '../common/undo';
 
 import {
@@ -59,7 +63,7 @@ interface INotebookModel extends DocumentRegistry.IModel {
    * #### Notes
    * This is a read-only property.
    */
-  cells: ObservableUndoableList<ICellModel>;
+  cells: IObservableUndoableVector<ICellModel>;
 
   /**
    * The cell model factory for the notebook.
@@ -149,7 +153,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
   constructor(options: NotebookModel.IOptions = {}) {
     super(options.languagePreference);
     this._factory = options.factory || NotebookModel.defaultFactory;
-    this._cells = new ObservableUndoableList<ICellModel>((data: nbformat.IBaseCell) => {
+    this._cells = new ObservableUndoableVector<ICellModel>((data: nbformat.IBaseCell) => {
       switch (data.cell_type) {
         case 'code':
           return this._factory.createCodeCell(data);
@@ -160,7 +164,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
       }
     });
     // Add an initial code cell by default.
-    this._cells.add(this._factory.createCodeCell());
+    this._cells.pushBack(this._factory.createCodeCell());
     this._cells.changed.connect(this._onCellsChanged, this);
     if (options.languagePreference) {
       this._metadata['language_info'] = { name: options.languagePreference };
@@ -178,7 +182,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
    * #### Notes
    * This is a read-only property.
    */
-  get cells(): ObservableUndoableList<ICellModel> {
+  get cells(): IObservableUndoableVector<ICellModel> {
     return this._cells;
   }
 
@@ -246,7 +250,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
     cells.dispose();
     clearSignalData(this);
     for (let i = 0; i < cells.length; i++) {
-      let cell = cells.get(i);
+      let cell = cells.at(i);
       cell.dispose();
     }
     cells.clear();
@@ -282,7 +286,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
   toJSON(): nbformat.INotebookContent {
     let cells: nbformat.ICell[] = [];
     for (let i = 0; i < this.cells.length; i++) {
-      let cell = this.cells.get(i);
+      let cell = this.cells.at(i);
       cells.push(cell.toJSON());
     }
     let metadata = utils.copy(this._metadata) as nbformat.INotebookMetadata;
@@ -317,7 +321,11 @@ class NotebookModel extends DocumentModel implements INotebookModel {
         continue;
       }
     }
-    this.cells.assign(cells);
+    this.cells.beginCompoundOperation();
+    this.cells.clear();
+    this.cells.pushAll(cells);
+    this.cells.endCompoundOperation();
+
     let oldValue = 0;
     let newValue = 0;
     if (value.nbformat !== this._nbformat) {
@@ -401,32 +409,25 @@ class NotebookModel extends DocumentModel implements INotebookModel {
   /**
    * Handle a change in the cells list.
    */
-  private _onCellsChanged(list: IObservableList<ICellModel>, change: IListChangedArgs<ICellModel>): void {
-    let cell: ICellModel;
+  private _onCellsChanged(list: IObservableVector<ICellModel>, change: ObservableVector.IChangedArgs<ICellModel>): void {
     switch (change.type) {
     case 'add':
-      cell = change.newValue as ICellModel;
-      cell.contentChanged.connect(this._onCellChanged, this);
+      each(change.newValues, cell => {
+        cell.contentChanged.connect(this._onCellChanged, this);
+      });
       break;
     case 'remove':
-      (change.oldValue as ICellModel).dispose();
-      break;
-    case 'replace':
-      let newValues = change.newValue as ICellModel[];
-      for (cell of newValues) {
-        cell.contentChanged.connect(this._onCellChanged, this);
-      }
-      let oldValues = change.oldValue as ICellModel[];
-      for (cell of oldValues) {
+      each(change.oldValues, cell => {
         cell.dispose();
-      }
+      });
       break;
     case 'set':
-      cell = change.newValue as ICellModel;
-      cell.contentChanged.connect(this._onCellChanged, this);
-      if (change.oldValue) {
-        (change.oldValue as ICellModel).dispose();
-      }
+      each(change.newValues, cell => {
+        cell.contentChanged.connect(this._onCellChanged, this);
+      });
+      each(change.oldValues, cell => {
+        cell.dispose();
+      });
       break;
     default:
       return;
@@ -436,8 +437,8 @@ class NotebookModel extends DocumentModel implements INotebookModel {
       // Add the cell in a new context to avoid triggering another
       // cell changed event during the handling of this signal.
       requestAnimationFrame(() => {
-        if (!this._cells.length) {
-          this._cells.add(this._factory.createCodeCell());
+        if (!this.isDisposed && !this._cells.length) {
+          this._cells.pushBack(this._factory.createCodeCell());
         }
       });
     }
@@ -453,7 +454,7 @@ class NotebookModel extends DocumentModel implements INotebookModel {
     this.contentChanged.emit(void 0);
   }
 
-  private _cells: ObservableUndoableList<ICellModel> = null;
+  private _cells: IObservableUndoableVector<ICellModel> = null;
   private _factory: ICellModelFactory = null;
   private _metadata: { [key: string]: any } = Private.createMetadata();
   private _cursors: { [key: string]: MetadataCursor } = Object.create(null);

+ 2 - 2
src/notebook/notebook/trust.ts

@@ -35,7 +35,7 @@ function trustNotebook(model: INotebookModel, host?: HTMLElement): Promise<void>
   let cells = model.cells;
   let trusted = true;
   for (let i = 0; i < cells.length; i++) {
-    let cell = cells.get(i);
+    let cell = cells.at(i);
     if (!cell.getMetadata('trusted').getValue()) {
       trusted = false;
     }
@@ -49,7 +49,7 @@ function trustNotebook(model: INotebookModel, host?: HTMLElement): Promise<void>
   }).then(result => {
     if (result.text === 'OK') {
       for (let i = 0; i < cells.length; i++) {
-        let cell = cells.get(i);
+        let cell = cells.at(i);
         cell.getMetadata('trusted').setValue(true);
       }
     }

+ 23 - 20
src/notebook/notebook/widget.ts

@@ -5,6 +5,10 @@ import {
   KernelMessage
 } from '@jupyterlab/services';
 
+import {
+  each
+} from 'phosphor/lib/algorithm/iteration';
+
 import {
   find
 } from 'phosphor/lib/algorithm/searching';
@@ -38,8 +42,8 @@ import {
 } from '../../common/interfaces';
 
 import {
-  IObservableList, IListChangedArgs
-} from '../../common/observablelist';
+  IObservableVector, ObservableVector
+} from '../../common/observablevector';
 
 import {
   InspectionHandler
@@ -323,7 +327,7 @@ class StaticNotebook extends Widget {
     this._mimetype = this._renderer.getCodeMimetype(newValue);
     let cells = newValue.cells;
     for (let i = 0; i < cells.length; i++) {
-      this._insertCell(i, cells.get(i));
+      this._insertCell(i, cells.at(i));
     }
     cells.changed.connect(this._onCellsChanged, this);
     newValue.contentChanged.connect(this.onModelContentChanged, this);
@@ -333,32 +337,31 @@ class StaticNotebook extends Widget {
   /**
    * Handle a change cells event.
    */
-  private _onCellsChanged(sender: IObservableList<ICellModel>, args: IListChangedArgs<ICellModel>) {
+  private _onCellsChanged(sender: IObservableVector<ICellModel>, args: ObservableVector.IChangedArgs<ICellModel>) {
+    let index = 0;
     switch (args.type) {
     case 'add':
-      this._insertCell(args.newIndex, args.newValue as ICellModel);
+      index = args.newIndex;
+      each(args.newValues, value => {
+        this._insertCell(index++, value);
+      });
       break;
     case 'move':
       this._moveCell(args.newIndex, args.oldIndex);
       break;
     case 'remove':
-      this._removeCell(args.oldIndex);
-      break;
-    case 'replace':
-      // TODO: reuse existing cell widgets if possible.
-      let oldValues = args.oldValue as ICellModel[];
-      for (let i = 0; i < oldValues.length; i++) {
+      each(args.oldValues, value => {
         this._removeCell(args.oldIndex);
-      }
-      let newValues = args.newValue as ICellModel[];
-      for (let i = newValues.length; i > 0; i--) {
-        this._insertCell(args.newIndex, newValues[i - 1]);
-      }
+      });
       break;
     case 'set':
-      // TODO: reuse existing widget if possible.
-      this._removeCell(args.newIndex);
-      this._insertCell(args.newIndex, args.newValue as ICellModel);
+      // TODO: reuse existing widgets if possible.
+      index = args.newIndex;
+      each(args.newValues, value => {
+        this._removeCell(index);
+        this._insertCell(index, value);
+        index++;
+      });
       break;
     default:
       return;
@@ -991,7 +994,7 @@ class Notebook extends StaticNotebook {
       return;
     }
     let layout = this.layout as PanelLayout;
-    let cell = model.cells.get(i) as MarkdownCellModel;
+    let cell = model.cells.at(i) as MarkdownCellModel;
     let widget = layout.widgets.at(i) as MarkdownCellWidget;
     if (cell.type === 'markdown') {
       widget.rendered = false;

+ 17 - 13
src/notebook/output-area/model.ts

@@ -9,6 +9,10 @@ import {
   JSONObject
 } from 'phosphor/lib/algorithm/json';
 
+import {
+  indexOf
+} from 'phosphor/lib/algorithm/searching';
+
 import {
   IDisposable
 } from 'phosphor/lib/core/disposable';
@@ -18,8 +22,8 @@ import {
 } from 'phosphor/lib/core/signaling';
 
 import {
-  IListChangedArgs, IObservableList, ObservableList
-} from '../../common/observablelist';
+  IObservableVector, ObservableVector
+} from '../../common/observablevector';
 
 import {
   nbformat
@@ -35,14 +39,14 @@ class OutputAreaModel implements IDisposable {
    * Construct a new observable outputs instance.
    */
   constructor() {
-    this.list = new ObservableList<OutputAreaModel.Output>();
+    this.list = new ObservableVector<OutputAreaModel.Output>();
     this.list.changed.connect(this._onListChanged, this);
   }
 
   /**
    * A signal emitted when the model changes.
    */
-  changed: ISignal<OutputAreaModel, IListChangedArgs<OutputAreaModel.Output>>;
+  changed: ISignal<OutputAreaModel, ObservableVector.IChangedArgs<OutputAreaModel.Output>>;
 
   /**
    * A signal emitted when the model is disposed.
@@ -86,7 +90,7 @@ class OutputAreaModel implements IDisposable {
    * Get an item at the specified index.
    */
   get(index: number): OutputAreaModel.Output {
-    return this.list.get(index);
+    return this.list.at(index);
   }
 
   /**
@@ -103,7 +107,7 @@ class OutputAreaModel implements IDisposable {
       this.clearNext = false;
     }
     if (output.output_type === 'input_request') {
-      this.list.add(output);
+      this.list.pushBack(output);
     }
 
     // Make a copy of the output bundle.
@@ -135,7 +139,7 @@ class OutputAreaModel implements IDisposable {
       case 'execute_result':
       case 'display_data':
       case 'error':
-        return this.list.add(value);
+        return this.list.pushBack(value);
       default:
         break;
       }
@@ -148,12 +152,12 @@ class OutputAreaModel implements IDisposable {
    *
    * @param wait Delay clearing the output until the next message is added.
    */
-  clear(wait: boolean = false): OutputAreaModel.Output[] {
+  clear(wait: boolean = false): void {
     if (wait) {
       this.clearNext = true;
-      return [];
+      return;
     }
-    return this.list.clear();
+    this.list.clear();
   }
 
   /**
@@ -171,7 +175,7 @@ class OutputAreaModel implements IDisposable {
    * Types are validated before being added.
    */
   addMimeData(output: nbformat.IDisplayData | nbformat.IExecuteResult, mimetype: string, value: string | JSONObject): void {
-    let index = this.list.indexOf(output);
+    let index = indexOf(this.list, output);
     if (index === -1) {
       throw new Error(`Cannot add data to non-tracked bundle`);
     }
@@ -255,12 +259,12 @@ class OutputAreaModel implements IDisposable {
   }
 
   protected clearNext = false;
-  protected list: IObservableList<OutputAreaModel.Output> = null;
+  protected list: IObservableVector<OutputAreaModel.Output> = null;
 
   /**
    * Handle a change to the list.
    */
-  private _onListChanged(sender: IObservableList<OutputAreaModel.Output>, args: IListChangedArgs<OutputAreaModel.Output>) {
+  private _onListChanged(sender: IObservableVector<OutputAreaModel.Output>, args: ObservableVector.IChangedArgs<OutputAreaModel.Output>) {
     this.changed.emit(args);
   }
 }

+ 10 - 8
src/notebook/output-area/widget.ts

@@ -5,6 +5,10 @@ import {
   IKernel
 } from '@jupyterlab/services';
 
+import {
+  IIterable, each
+} from 'phosphor/lib/algorithm/iteration';
+
 import {
   JSONObject
 } from 'phosphor/lib/algorithm/json';
@@ -34,8 +38,8 @@ import {
 } from 'phosphor/lib/ui/widget';
 
 import {
-  IListChangedArgs
-} from '../../common/observablelist';
+  ObservableVector
+} from '../../common/observablevector';
 
 import {
   RenderMime
@@ -361,13 +365,13 @@ class OutputAreaWidget extends Widget {
   /**
    * Follow changes on the model state.
    */
-  protected onModelStateChanged(sender: OutputAreaModel, args: IListChangedArgs<nbformat.IOutput>) {
+  protected onModelStateChanged(sender: OutputAreaModel, args: ObservableVector.IChangedArgs<nbformat.IOutput>) {
     switch (args.type) {
     case 'add':
       // Children are always added at the end.
       this.addChild();
       break;
-    case 'replace':
+    case 'remove':
       // Only "clear" is supported by the model.
       // When an output area is cleared and then quickly replaced with new
       // content (as happens with @interact in widgets, for example), the
@@ -385,11 +389,9 @@ class OutputAreaWidget extends Widget {
         }
         this.node.style.minHeight = '';
       }, 50);
-
-      let oldValues = args.oldValue as nbformat.IOutput[];
-      for (let i = args.oldIndex; i < oldValues.length; i++) {
+      each(args.oldValues, value => {
         this.removeChild(args.oldIndex);
-      }
+      });
       break;
     case 'set':
       if (!this._injecting) {

+ 0 - 639
test/src/common/observablelist.spec.ts

@@ -1,639 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import expect = require('expect.js');
-
-import {
-  toArray
-} from 'phosphor/lib/algorithm/iteration';
-
-import {
-  ObservableList
-} from '../../../lib/common/observablelist';
-
-
-class LoggingList extends ObservableList<number> {
-
-  methods: string[] = [];
-
-  get internalArray(): number[] {
-    return toArray(this.internal);
-  }
-
-  protected addItem(index: number, item: number): number {
-    this.methods.push('addItem');
-    return super.addItem(index, item);
-  }
-
-  protected moveItem(fromIndex: number, toIndex: number): boolean {
-    this.methods.push('moveItem');
-    return super.moveItem(fromIndex, toIndex);
-  }
-
-  protected replaceItems(index: number, count: number, items: number[]): number[] {
-    this.methods.push('replaceItems');
-    return super.replaceItems(index, count, items);
-  }
-
-  protected setItem(index: number, item: number): number {
-    this.methods.push('setItem');
-    return super.setItem(index, item);
-  }
-}
-
-
-describe('common/observablelist', () => {
-
-  describe('ObservableList', () => {
-
-    describe('#constructor()', () => {
-
-      it('should accept no arguments', () => {
-        let list = new ObservableList<number>();
-        expect(list instanceof ObservableList).to.be(true);
-      });
-
-      it('should accept an array argument', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list instanceof ObservableList).to.be(true);
-      });
-
-      it('should initialize the list items', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice()).to.eql([1, 2, 3]);
-      });
-
-    });
-
-    describe('#changed', () => {
-
-      it('should be emitted when the list changes state', () => {
-        let called = false;
-        let list = new ObservableList<number>();
-        list.changed.connect(() => { called = true; });
-        list.insert(0, 1);
-        expect(called).to.be(true);
-      });
-
-      it('should have list changed args', () => {
-        let called = false;
-        let list = new ObservableList<number>();
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'add',
-            newIndex: 0,
-            newValue: 1,
-            oldIndex: -1,
-            oldValue: void 0
-          });
-          called = true;
-        });
-        list.add(1);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#length', () => {
-
-      it('should give the number of items in the list', () => {
-        let list = new ObservableList<number>();
-        expect(list.length).to.be(0);
-        list.insert(0, 1);
-        expect(list.length).to.be(1);
-      });
-
-    });
-
-    describe('#get()', () => {
-
-      it('should get the item at a specific index in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.get(0)).to.be(1);
-        expect(list.get(1)).to.be(2);
-        expect(list.get(2)).to.be(3);
-      });
-
-      it('should offset from the end of the list if index is negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.get(-1)).to.be(3);
-        expect(list.get(-2)).to.be(2);
-        expect(list.get(-3)).to.be(1);
-      });
-
-      it('should return `undefined` if the index is out of range', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.get(3)).to.be(void 0);
-      });
-
-    });
-
-    describe('#indexOf()', () => {
-
-      it('should get the index of the first occurence of an item in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 3]);
-        expect(list.indexOf(1)).to.be(0);
-        expect(list.indexOf(2)).to.be(1);
-        expect(list.indexOf(3)).to.be(2);
-      });
-
-      it('should return `-1` if the item is not in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 3]);
-        expect(list.indexOf(4)).to.be(-1);
-      });
-
-    });
-
-    describe('#contains()', () => {
-
-      it('should test whether the list contains a specific item', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.contains(1)).to.be(true);
-        expect(list.contains(4)).to.be(false);
-      });
-
-    });
-
-    describe('#slice()', () => {
-
-      it('should get a shallow copy of a portion of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice()).to.eql([1, 2, 3]);
-        expect(list.slice()).to.not.be(list.slice());
-      });
-
-      it('should index start from the end if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice(-1)).to.eql([3]);
-      });
-
-      it('should clamp start to the bounds of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice(4)).to.eql([]);
-      });
-
-      it('should default start to `0`', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice()).to.eql([1, 2, 3]);
-      });
-
-      it('should index end from the end if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice(1, -1)).to.eql([2]);
-      });
-
-      it('should clamp end to the bounds of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice(1, 4)).to.eql([2, 3]);
-      });
-
-      it('should default end to the length of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.slice(1)).to.eql([2, 3]);
-      });
-
-    });
-
-    describe('#set()', () => {
-
-      it('should set the item at a specific index', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.set(1, 4);
-        expect(list.slice()).to.eql([1, 4, 3]);
-      });
-
-      it('should index from the end if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.set(-1, 4);
-        expect(list.slice()).to.eql([1, 2, 4]);
-      });
-
-      it('should return the item which occupied the index', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.set(1, 4)).to.be(2);
-      });
-
-      it('should return `undefined` if the index is out of range', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.set(4, 4)).to.be(void 0);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'set',
-            newIndex: 1,
-            newValue: 4,
-            oldIndex: 1,
-            oldValue: 2
-          });
-          called = true;
-        });
-        list.set(1, 4);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#assign()', () => {
-
-      it('should replace all items in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.assign([9, 8, 7, 6]);
-        expect(list.slice()).to.eql([9, 8, 7, 6]);
-      });
-
-      it('should return the old items', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4]);
-        expect(list.assign([9, 8, 7, 6])).to.eql([1, 2, 3, 4]);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          console.log('1 HERE I AM');
-          expect(args).to.eql({
-            type: 'replace',
-            newIndex: 0,
-            newValue: [9, 8, 7, 6],
-            oldIndex: 0,
-            oldValue: [1, 2, 3, 4, 5, 6]
-          });
-          console.log('2 HERE I AM');
-          called = true;
-        });
-        list.assign([9, 8, 7, 6]);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#add()', () => {
-
-      it('should add an item to the end of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.add(4);
-        expect(list.slice()).to.eql([1, 2, 3, 4]);
-      });
-
-      it('should return the new index of the item in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.add(4)).to.be(3);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'add',
-            newIndex: 3,
-            newValue: 4,
-            oldIndex: -1,
-            oldValue: void 0
-          });
-          called = true;
-        });
-        list.add(4);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#insert()', () => {
-
-      it('should insert an item into the list at a specific index', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.insert(1, 4);
-        expect(list.slice()).to.eql([1, 4, 2, 3]);
-      });
-
-      it('should index from the end of list if the index is negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.insert(-1, 4);
-        expect(list.slice()).to.eql([1, 2, 4, 3]);
-      });
-
-      it('should clamp to the bounds of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.insert(10, 4);
-        expect(list.slice()).to.eql([1, 2, 3, 4]);
-      });
-
-      it('should return the new index of the item in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.insert(10, 4)).to.be(3);
-        expect(list.insert(-2, 9)).to.be(2);
-        expect(list.insert(-10, 5)).to.be(0);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'add',
-            newIndex: 1,
-            newValue: 4,
-            oldIndex: -1,
-            oldValue: void 0
-          });
-          called = true;
-        });
-        list.insert(1, 4);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#move()', () => {
-
-      it('should move an item from one index to another', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.move(1, 2);
-        expect(list.slice()).to.eql([1, 3, 2]);
-      });
-
-      it('should index fromIndex from the end of list if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.move(-1, 1);
-        expect(list.slice()).to.eql([1, 3, 2]);
-      });
-
-      it('should index toIndex from the end of list if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.move(0, -1);
-        expect(list.slice()).to.eql([2, 3, 1]);
-      });
-
-      it('should return `true` if the item was moved', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.move(0, 1)).to.be(true);
-      });
-
-      it('should return `false` if the either index is out of range', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.move(10, 1)).to.be(false);
-        expect(list.move(1, 10)).to.be(false);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'move',
-            newIndex: 1,
-            newValue: 1,
-            oldIndex: 0,
-            oldValue: 1
-          });
-          called = true;
-        });
-        list.move(0, 1);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#remove()', () => {
-
-      it('should remove the first occurrence of a specific item from the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.remove(1);
-        expect(list.slice()).to.eql([2, 3]);
-      });
-
-      it('should return the index occupied by the item', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.remove(1)).to.be(0);
-      });
-
-      it('should return `-1` if the item is not in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.remove(10)).to.be(-1);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'remove',
-            newIndex: -1,
-            newValue: void 0,
-            oldIndex: 1,
-            oldValue: 2
-          });
-          called = true;
-        });
-        list.remove(2);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#removeAt()', () => {
-
-      it('should remove the item at a specific index', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.removeAt(1);
-        expect(list.slice()).to.eql([1, 3]);
-      });
-
-      it('should index from the end of list if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.removeAt(-1);
-        expect(list.slice()).to.eql([1, 2]);
-      });
-
-      it('should return the item at the specified index', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.removeAt(1)).to.be(2);
-      });
-
-      it('should return `undefined` if the index is out of range', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        expect(list.removeAt(10)).to.be(void 0);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'remove',
-            newIndex: -1,
-            newValue: void 0,
-            oldIndex: 1,
-            oldValue: 2
-          });
-          called = true;
-        });
-        list.removeAt(1);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#replace()', () => {
-
-      it('should replace items at a specific location in the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.replace(1, 2, [4, 5, 6]);
-        expect(list.slice()).to.eql([1, 4, 5, 6]);
-      });
-
-      it('should index from the end of the list if negative', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.replace(-2, 2, [4, 5, 6]);
-        expect(list.slice()).to.eql([1, 4, 5, 6]);
-      });
-
-      it('should clamp the index to the bounds of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3]);
-        list.replace(10, 2, [4, 5, 6]);
-        expect(list.slice()).to.eql([1, 2, 3, 4, 5, 6]);
-      });
-
-      it('should remove the given count of items', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.replace(0, 3, [1, 2]);
-        expect(list.slice()).to.eql([1, 2, 4, 5, 6]);
-      });
-
-      it('should clamp the count to the length of the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.replace(0, 10, [1, 2]);
-        expect(list.slice()).to.eql([1, 2]);
-      });
-
-      it('should handle an empty items array', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.replace(1, 10, []);
-        expect(list.slice()).to.eql([1]);
-      });
-
-      it('should return an array of items removed from the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        expect(list.replace(1, 3, [])).to.eql([2, 3, 4]);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'replace',
-            newIndex: 0,
-            newValue: [],
-            oldIndex: 0,
-            oldValue: [1, 2, 3, 4, 5, 6]
-          });
-          called = true;
-        });
-        list.replace(0, 10, []);
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#clear()', () => {
-
-      it('should remove all items from the list', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.clear();
-        expect(list.length).to.be(0);
-        list.clear();
-        expect(list.length).to.be(0);
-      });
-
-      it('should return the removed items', () => {
-        let list = new ObservableList<number>([1, 2, 3, 4]);
-        expect(list.clear()).to.eql([1, 2, 3, 4]);
-      });
-
-      it('should trigger a changed signal', () => {
-        let called = false;
-        let list = new ObservableList<number>([1, 2, 3, 4, 5, 6]);
-        list.changed.connect((sender, args) => {
-          expect(sender).to.be(list);
-          expect(args).to.eql({
-            type: 'replace',
-            newIndex: 0,
-            newValue: [],
-            oldIndex: 0,
-            oldValue: [1, 2, 3, 4, 5, 6]
-          });
-          called = true;
-        });
-        list.clear();
-        expect(called).to.be(true);
-      });
-
-    });
-
-    describe('#internal', () => {
-
-      it('should be the protected internal array of items for the list', () => {
-        let list = new LoggingList([1, 2, 3]);
-        expect(list.internalArray).to.eql([1, 2, 3]);
-      });
-
-    });
-
-    describe('#addItem()', () => {
-
-      it('should be called when we add an item at the specified index', () => {
-        let list = new LoggingList([1, 2, 3]);
-        list.add(1);
-        expect(list.methods.indexOf('addItem')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#moveItem()', () => {
-
-      it('should be called when we move an item from one index to another', () => {
-        let list = new LoggingList([1, 2, 3]);
-        list.move(1, 0);
-        expect(list.methods.indexOf('moveItem')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#replaceItems()', () => {
-
-      it('should be called when we replace items at a specific location in the list', () => {
-        let list = new LoggingList([1, 2, 3]);
-        list.replace(1, 1, []);
-        expect(list.methods.indexOf('replaceItems')).to.not.be(-1);
-      });
-
-    });
-
-    describe('#setItem()', () => {
-
-      it('should be called when we set the item at a specific index', () => {
-        let list = new LoggingList([1, 2, 3]);
-        list.set(1, 4);
-        expect(list.methods.indexOf('setItem')).to.not.be(-1);
-      });
-
-    });
-
-  });
-
-});

+ 427 - 0
test/src/common/observablevector.spec.ts

@@ -0,0 +1,427 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  toArray
+} from 'phosphor/lib/algorithm/iteration';
+
+import {
+  ObservableVector
+} from '../../../lib/common/observablevector';
+
+
+describe('common/ObservableVector', () => {
+
+  describe('ObservableVector', () => {
+
+    describe('#constructor()', () => {
+
+      it('should accept no arguments', () => {
+        let value = new ObservableVector<number>();
+        expect(value instanceof ObservableVector).to.be(true);
+      });
+
+      it('should accept an array argument', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value instanceof ObservableVector).to.be(true);
+      });
+
+      it('should initialize the vector items', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(toArray(value)).to.eql([1, 2, 3]);
+      });
+
+    });
+
+    describe('#changed', () => {
+
+      it('should be emitted when the vector changes state', () => {
+        let called = false;
+        let value = new ObservableVector<number>();
+        value.changed.connect(() => { called = true; });
+        value.insert(0, 1);
+        expect(called).to.be(true);
+      });
+
+      it('should have value changed args', () => {
+        let called = false;
+        let value = new ObservableVector<number>();
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('add');
+          expect(args.newIndex).to.be(0);
+          expect(args.oldIndex).to.be(-1);
+          expect(args.newValues.at(0)).to.be(1);
+          expect(args.oldValues.length).to.be(0);
+          called = true;
+        });
+        value.pushBack(1);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#isDisposed', () => {
+
+      it('should test whether the vector is disposed', () => {
+        let value = new ObservableVector<number>();
+        expect(value.isDisposed).to.be(false);
+        value.dispose();
+        expect(value.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#dispose()', () => {
+
+      it('should dispose of the resources held by the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.dispose();
+        expect(value.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#set()', () => {
+
+      it('should set the item at a specific index', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.set(1, 4);
+        expect(toArray(value)).to.eql([1, 4, 3]);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('set');
+          expect(args.newIndex).to.be(1);
+          expect(args.oldIndex).to.be(1);
+          expect(args.oldValues.at(0)).to.be(2);
+          expect(args.newValues.at(0)).to.be(4);
+          called = true;
+        });
+        value.set(1, 4);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#pushBack()', () => {
+
+      it('should add an item to the end of the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.pushBack(4);
+        expect(toArray(value)).to.eql([1, 2, 3, 4]);
+      });
+
+      it('should return the new length of the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.pushBack(4)).to.be(4);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('add');
+          expect(args.newIndex).to.be(3);
+          expect(args.oldIndex).to.be(-1);
+          expect(args.oldValues.length).to.be(0);
+          expect(args.newValues.at(0)).to.be(4);
+          called = true;
+        });
+        value.pushBack(4);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#popBack()', () => {
+
+      it('should remove an item from the end of the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.popBack();
+        expect(toArray(value)).to.eql([1, 2]);
+      });
+
+      it('should return the removed value', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.popBack()).to.be(3);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('remove');
+          expect(args.newIndex).to.be(-1);
+          expect(args.oldIndex).to.be(2);
+          expect(args.oldValues.at(0)).to.be(3);
+          expect(args.newValues.length).to.be(0);
+          called = true;
+        });
+        value.popBack();
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#insert()', () => {
+
+      it('should insert an item into the vector at a specific index', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.insert(1, 4);
+        expect(toArray(value)).to.eql([1, 4, 2, 3]);
+      });
+
+      it('should return the new length in the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.insert(1, 4)).to.be(4);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('add');
+          expect(args.newIndex).to.be(1);
+          expect(args.oldIndex).to.be(-1);
+          expect(args.oldValues.length).to.be(0);
+          expect(args.newValues.at(0)).to.be(4);
+          called = true;
+        });
+        value.insert(1, 4);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#move()', () => {
+
+      it('should move an item from one index to another', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.move(1, 2);
+        expect(toArray(value)).to.eql([1, 3, 2]);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('move');
+          expect(args.newIndex).to.be(1);
+          expect(args.oldIndex).to.be(0);
+          expect(args.oldValues.at(0)).to.be(1);
+          expect(args.newValues.at(0)).to.be(1);
+          called = true;
+        });
+        value.move(0, 1);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#remove()', () => {
+
+      it('should remove the first occurrence of a specific item from the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.remove(1);
+        expect(toArray(value)).to.eql([2, 3]);
+      });
+
+      it('should return the index occupied by the item', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.remove(1)).to.be(0);
+      });
+
+      it('should return `-1` if the item is not in the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.remove(10)).to.be(-1);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('remove');
+          expect(args.newIndex).to.be(-1);
+          expect(args.oldIndex).to.be(1);
+          expect(args.oldValues.at(0)).to.be(2);
+          expect(args.newValues.length).to.be(0);
+          called = true;
+        });
+        value.remove(2);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#removeAt()', () => {
+
+      it('should remove the item at a specific index', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.removeAt(1);
+        expect(toArray(value)).to.eql([1, 3]);
+      });
+
+      it('should return the item at the specified index', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.removeAt(1)).to.be(2);
+      });
+
+      it('should return `undefined` if the index is out of range', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.removeAt(10)).to.be(void 0);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('remove');
+          expect(args.newIndex).to.be(-1);
+          expect(args.oldIndex).to.be(1);
+          expect(args.oldValues.at(0)).to.be(2);
+          expect(args.newValues.length).to.be(0);
+          called = true;
+        });
+        value.removeAt(1);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#clear()', () => {
+
+      it('should remove all items from the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.clear();
+        expect(value.length).to.be(0);
+        value.clear();
+        expect(value.length).to.be(0);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('remove');
+          expect(args.newIndex).to.be(0);
+          expect(args.oldIndex).to.be(0);
+          expect(toArray(args.oldValues)).to.eql([1, 2, 3, 4, 5, 6]);
+          expect(args.newValues.length).to.be(0);
+          called = true;
+        });
+        value.clear();
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#pushAll()', () => {
+
+      it('should push an array of items to the end of the vector', () => {
+        let value = new ObservableVector<number>([1]);
+        value.pushAll([2, 3, 4]);
+        expect(toArray(value)).to.eql([1, 2, 3, 4]);
+      });
+
+      it('should return the new length of the vector', () => {
+        let value = new ObservableVector<number>([1]);
+        expect(value.pushAll([2, 3, 4])).to.be(4);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('add');
+          expect(args.newIndex).to.be(3);
+          expect(args.oldIndex).to.be(-1);
+          expect(toArray(args.newValues)).to.eql([4, 5, 6]);
+          expect(args.oldValues.length).to.be(0);
+          called = true;
+        });
+        value.pushAll([4, 5, 6]);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#insertAll()', () => {
+
+      it('should push an array of items into a vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.insertAll(1, [2, 3, 4]);
+        expect(toArray(value)).to.eql([1, 2, 3, 4, 2, 3]);
+      });
+
+      it('should return the new length of the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3]);
+        expect(value.insertAll(1, [2, 3, 4])).to.be(6);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('add');
+          expect(args.newIndex).to.be(1);
+          expect(args.oldIndex).to.be(-1);
+          expect(toArray(args.newValues)).to.eql([4, 5, 6]);
+          expect(args.oldValues.length).to.be(0);
+          called = true;
+        });
+        value.insertAll(1, [4, 5, 6]);
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#removeRange()', () => {
+
+      it('should remove a range of items from the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        value.removeRange(1, 3);
+        expect(toArray(value)).to.eql([1, 4, 5, 6]);
+      });
+
+      it('should return the new length of the vector', () => {
+        let value = new ObservableVector<number>([1, 2, 3, 4, 5, 6]);
+        expect(value.removeRange(1, 3)).to.be(4);
+      });
+
+      it('should trigger a changed signal', () => {
+        let called = false;
+        let value = new ObservableVector<number>([1, 2, 3, 4]);
+        value.changed.connect((sender, args) => {
+          expect(sender).to.be(value);
+          expect(args.type).to.be('remove');
+          expect(args.newIndex).to.be(-1);
+          expect(args.oldIndex).to.be(1);
+          expect(toArray(args.oldValues)).to.eql([2, 3]);
+          expect(args.newValues.length).to.be(0);
+          called = true;
+        });
+        value.removeRange(1, 3);
+        expect(called).to.be(true);
+      });
+
+    });
+
+  });
+
+});

+ 3 - 1
test/src/index.ts

@@ -3,7 +3,7 @@
 
 import './common/activitymonitor.spec';
 import './common/instancetracker.spec';
-import './common/observablelist.spec';
+import './common/observablevector.spec';
 import './common/vdom.spec';
 
 import './completer/handler.spec';
@@ -33,6 +33,8 @@ import './notebook/cells/editor.spec';
 import './notebook/cells/model.spec';
 import './notebook/cells/widget.spec';
 
+import './notebook/common/undo.spec';
+
 import './notebook/notebook/actions.spec';
 import './notebook/notebook/default-toolbar.spec';
 import './notebook/notebook/model.spec';

+ 242 - 0
test/src/notebook/common/undo.spec.ts

@@ -0,0 +1,242 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
+
+import {
+  ObservableUndoableVector, ISerializable
+} from '../../../../lib/notebook/common/undo';
+
+
+
+class Test implements ISerializable {
+
+  constructor(value: JSONObject) {
+    this._value = value;
+  }
+
+  toJSON(): JSONObject {
+    return this._value;
+  }
+
+  private _value: JSONObject;
+}
+
+
+let count = 0;
+
+function factory(value: JSONObject): Test {
+  value['count'] = count++;
+  return new Test(value);
+}
+
+
+const value: JSONObject = { name: 'foo' };
+
+
+describe('notebook/common/undo', () => {
+
+  describe('ObservableUndoableVector', () => {
+
+    describe('#constructor', () => {
+
+      it('should create a new ObservableUndoableVector', () => {
+        let vector = new ObservableUndoableVector(factory);
+        expect(vector).to.be.an(ObservableUndoableVector);
+      });
+
+    });
+
+    describe('#canRedo', () => {
+
+      it('should return false if there is no history', () => {
+        let vector = new ObservableUndoableVector(factory);
+        expect(vector.canRedo).to.be(false);
+      });
+
+      it('should return true if there is an undo that can be redone', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushBack(new Test(value));
+        vector.undo();
+        expect(vector.canRedo).to.be(true);
+      });
+
+    });
+
+    describe('#canUndo', () => {
+
+      it('should return false if there is no history', () => {
+        let vector = new ObservableUndoableVector(factory);
+        expect(vector.canUndo).to.be(false);
+      });
+
+      it('should return true if there is a change that can be undone', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushBack(factory(value));
+        expect(vector.canUndo).to.be(true);
+      });
+
+    });
+
+    describe('#dispose()', () => {
+
+      it('should dispose of the resources used by the vector', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.dispose();
+        expect(vector.isDisposed).to.be(true);
+        vector.dispose();
+        expect(vector.isDisposed).to.be(true);
+      });
+
+    });
+
+    describe('#beginCompoundOperation()', () => {
+
+      it('should begin a compound operation', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.beginCompoundOperation();
+        vector.pushBack(factory(value));
+        vector.pushBack(factory(value));
+        vector.endCompoundOperation();
+        expect(vector.canUndo).to.be(true);
+        vector.undo();
+        expect(vector.canUndo).to.be(false);
+      });
+
+      it('should not be undoable if isUndoAble is set to false', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.beginCompoundOperation(false);
+        vector.pushBack(factory(value));
+        vector.pushBack(factory(value));
+        vector.endCompoundOperation();
+        expect(vector.canUndo).to.be(false);
+      });
+
+    });
+
+    describe('#endCompoundOperation()', () => {
+
+      it('should end a compound operation', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.beginCompoundOperation();
+        vector.pushBack(factory(value));
+        vector.pushBack(factory(value));
+        vector.endCompoundOperation();
+        expect(vector.canUndo).to.be(true);
+        vector.undo();
+        expect(vector.canUndo).to.be(false);
+      });
+
+    });
+
+    describe('#undo()', () => {
+
+      it('should undo a pushBack', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushBack(factory(value));
+        vector.undo();
+        expect(vector.length).to.be(0);
+      });
+
+      it('should undo a pushAll', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll([factory(value), factory(value)]);
+        vector.undo();
+        expect(vector.length).to.be(0);
+      });
+
+      it('should undo a remove', () => {
+         let vector = new ObservableUndoableVector(factory);
+         vector.pushAll([factory(value), factory(value)]);
+         vector.removeAt(0);
+         vector.undo();
+         expect(vector.length).to.be(2);
+      });
+
+      it('should undo a removeRange', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll([factory(value), factory(value), factory(value),
+          factory(value), factory(value), factory(value)]);
+        vector.removeRange(1, 3);
+        vector.undo();
+        expect(vector.length).to.be(6);
+      });
+
+      it('should undo a move', () => {
+        let items = [factory(value), factory(value), factory(value)];
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll(items);
+        vector.move(1, 2);
+        vector.undo();
+        expect((vector.at(1) as any)['count']).to.be((items[1] as any)['count']);
+      });
+
+    });
+
+    describe('#redo()', () => {
+
+      it('should redo a pushBack', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushBack(factory(value));
+        vector.undo();
+        vector.redo();
+        expect(vector.length).to.be(1);
+      });
+
+      it('should redo a pushAll', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll([factory(value), factory(value)]);
+        vector.undo();
+        vector.redo();
+        expect(vector.length).to.be(2);
+      });
+
+      it('should redo a remove', () => {
+         let vector = new ObservableUndoableVector(factory);
+         vector.pushAll([factory(value), factory(value)]);
+         vector.removeAt(0);
+         vector.undo();
+         vector.redo();
+         expect(vector.length).to.be(1);
+      });
+
+      it('should redo a removeRange', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll([factory(value), factory(value), factory(value),
+          factory(value), factory(value), factory(value)]);
+        vector.removeRange(1, 3);
+        vector.undo();
+        vector.redo();
+        expect(vector.length).to.be(4);
+      });
+
+      it('should undo a move', () => {
+        let items = [factory(value), factory(value), factory(value)];
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushAll(items);
+        vector.move(1, 2);
+        vector.undo();
+        vector.redo();
+        expect((vector.at(2) as any)['count']).to.be((items[1] as any)['count']);
+      });
+
+    });
+
+    describe('#clearUndo()', () => {
+
+      it('should clear the undo stack', () => {
+        let vector = new ObservableUndoableVector(factory);
+        vector.pushBack(factory(value));
+        vector.clearUndo();
+        expect(vector.canUndo).to.be(false);
+      });
+
+    });
+
+  });
+
+});

+ 11 - 10
test/src/notebook/notebook/actions.spec.ts

@@ -87,7 +87,7 @@ describe('notebook/notebook/actions', () => {
         cell.editor.setCursorPosition(10);
         NotebookActions.splitCell(widget);
         let cells = widget.model.cells;
-        let newSource = cells.get(index).source + cells.get(index + 1).source;
+        let newSource = cells.at(index).source + cells.at(index + 1).source;
         expect(newSource).to.be(source);
       });
 
@@ -171,6 +171,7 @@ describe('notebook/notebook/actions', () => {
         widget.select(next);
         source += next.model.source;
         let count = widget.childCount();
+        debugger;
         NotebookActions.mergeCells(widget);
         expect(widget.childCount()).to.be(count - 2);
         expect(widget.activeCell.model.source).to.be(source);
@@ -521,7 +522,7 @@ describe('notebook/notebook/actions', () => {
         widget.model.cells.insert(2, cell);
         widget.select(widget.childAt(2));
         cell = widget.model.factory.createCodeCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         widget.select(widget.childAt(widget.childCount() - 1));
         NotebookActions.run(widget, kernel).then(result => {
           expect(result).to.be(false);
@@ -532,7 +533,7 @@ describe('notebook/notebook/actions', () => {
 
       it('should render all markdown cells on an error', () => {
         let cell = widget.model.factory.createMarkdownCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         let child = widget.childAt(widget.childCount() - 1) as MarkdownCellWidget;
         child.rendered = false;
         widget.select(child);
@@ -624,7 +625,7 @@ describe('notebook/notebook/actions', () => {
       it('should stop executing code cells on an error', (done) => {
         widget.activeCell.model.source = ERROR_INPUT;
         let cell = widget.model.factory.createCodeCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         widget.select(widget.childAt(widget.childCount() - 1));
         NotebookActions.runAndAdvance(widget, kernel).then(result => {
           expect(result).to.be(false);
@@ -712,7 +713,7 @@ describe('notebook/notebook/actions', () => {
       it('should stop executing code cells on an error', (done) => {
         widget.activeCell.model.source = ERROR_INPUT;
         let cell = widget.model.factory.createCodeCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         widget.select(widget.childAt(widget.childCount() - 1));
         NotebookActions.runAndInsert(widget, kernel).then(result => {
           expect(result).to.be(false);
@@ -793,7 +794,7 @@ describe('notebook/notebook/actions', () => {
       it('should stop executing code cells on an error', (done) => {
         widget.activeCell.model.source = ERROR_INPUT;
         let cell = widget.model.factory.createCodeCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         NotebookActions.runAll(widget, kernel).then(result => {
           expect(result).to.be(false);
           expect(cell.executionCount).to.be(null);
@@ -1010,9 +1011,9 @@ describe('notebook/notebook/actions', () => {
         widget.activeCellIndex++;
         let source = widget.activeCell.model.source;
         NotebookActions.moveUp(widget);
-        expect(widget.model.cells.get(0).source).to.be(source);
+        expect(widget.model.cells.at(0).source).to.be(source);
         NotebookActions.undo(widget);
-        expect(widget.model.cells.get(1).source).to.be(source);
+        expect(widget.model.cells.at(1).source).to.be(source);
       });
 
     });
@@ -1043,9 +1044,9 @@ describe('notebook/notebook/actions', () => {
       it('should be undo-able', () => {
         let source = widget.activeCell.model.source;
         NotebookActions.moveDown(widget);
-        expect(widget.model.cells.get(1).source).to.be(source);
+        expect(widget.model.cells.at(1).source).to.be(source);
         NotebookActions.undo(widget);
-        expect(widget.model.cells.get(0).source).to.be(source);
+        expect(widget.model.cells.at(0).source).to.be(source);
       });
 
     });

+ 0 - 11
test/src/notebook/notebook/default-toolbar.spec.ts

@@ -322,17 +322,6 @@ describe('notebook/notebook/default-toolbar', () => {
         expect(item.node.textContent).to.be('No Kernel!');
       });
 
-      it('should handle a change in kernel', (done) => {
-        let item = createKernelNameItem(panel);
-        let name = context.kernelspecs.default;
-        panel.context.changeKernel({ name }).then(kernel => {
-          return kernel.getSpec().then(spec => {
-            expect(item.node.textContent).to.be(spec.display_name);
-            done();
-          });
-        }).catch(done);
-      });
-
       it('should handle a change in context', (done) => {
         let item = createKernelNameItem(panel);
         panel.kernel.getSpec().then(spec => {

+ 18 - 23
test/src/notebook/notebook/model.spec.ts

@@ -4,12 +4,12 @@
 import expect = require('expect.js');
 
 import {
-  CodeCellModel
-} from '../../../../lib/notebook/cells/model';
+  indexOf
+} from 'phosphor/lib/algorithm/searching';
 
 import {
-  ObservableUndoableList
-} from '../../../../lib/notebook/common/undo';
+  CodeCellModel
+} from '../../../../lib/notebook/cells/model';
 
 import {
   nbformat
@@ -45,7 +45,7 @@ describe('notebook/notebook/model', () => {
       it('should add a single code cell by default', () => {
         let model = new NotebookModel();
         expect(model.cells.length).to.be(1);
-        expect(model.cells.get(0)).to.be.a(CodeCellModel);
+        expect(model.cells.at(0)).to.be.a(CodeCellModel);
       });
 
       it('should accept an optional factory', () => {
@@ -87,23 +87,18 @@ describe('notebook/notebook/model', () => {
 
     describe('#cells', () => {
 
-      it('should be an observable undoable list', () => {
-        let model = new NotebookModel();
-        expect(model.cells).to.be.an(ObservableUndoableList);
-      });
-
       it('should add an empty code cell by default', () => {
         let model = new NotebookModel();
         expect(model.cells.length).to.be(1);
-        expect(model.cells.get(0)).to.be.a(CodeCellModel);
+        expect(model.cells.at(0)).to.be.a(CodeCellModel);
       });
 
       it('should be reset when loading from disk', () => {
         let model = new NotebookModel();
         let cell = model.factory.createCodeCell();
-        model.cells.add(cell);
+        model.cells.pushBack(cell);
         model.fromJSON(DEFAULT_CONTENT);
-        expect(model.cells.indexOf(cell)).to.be(-1);
+        expect(indexOf(model.cells, cell)).to.be(-1);
         expect(model.cells.length).to.be(6);
       });
 
@@ -111,12 +106,12 @@ describe('notebook/notebook/model', () => {
         let model = new NotebookModel();
         let cell = model.factory.createCodeCell();
         cell.source = 'foo';
-        model.cells.add(cell);
+        model.cells.pushBack(cell);
         model.fromJSON(DEFAULT_CONTENT);
         model.cells.undo();
         expect(model.cells.length).to.be(2);
-        expect(model.cells.get(1).source).to.be('foo');
-        expect(model.cells.get(1)).to.not.be(cell);  // should be a clone.
+        expect(model.cells.at(1).source).to.be('foo');
+        expect(model.cells.at(1)).to.not.be(cell);  // should be a clone.
       });
 
       context('cells `changed` signal', () => {
@@ -126,21 +121,21 @@ describe('notebook/notebook/model', () => {
           let cell = model.factory.createCodeCell();
           let called = false;
           model.contentChanged.connect(() => { called = true; });
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           expect(called).to.be(true);
         });
 
         it('should set the dirty flag', () => {
           let model = new NotebookModel();
           let cell = model.factory.createCodeCell();
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           expect(model.dirty).to.be(true);
         });
 
         it('should dispose of old cells', () => {
           let model = new NotebookModel();
           let cell = model.factory.createCodeCell();
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           model.cells.clear();
           expect(cell.isDisposed).to.be(true);
         });
@@ -150,7 +145,7 @@ describe('notebook/notebook/model', () => {
           model.cells.clear();
           requestAnimationFrame(() => {
             expect(model.cells.length).to.be(1);
-            expect(model.cells.get(0)).to.be.a(CodeCellModel);
+            expect(model.cells.at(0)).to.be.a(CodeCellModel);
             done();
           });
         });
@@ -162,14 +157,14 @@ describe('notebook/notebook/model', () => {
         it('should be called when a cell content changes', () => {
           let model = new NotebookModel();
           let cell = model.factory.createCodeCell();
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           cell.source = 'foo';
         });
 
         it('should emit the `contentChanged` signal', () => {
           let model = new NotebookModel();
           let cell = model.factory.createCodeCell();
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           let called = false;
           model.contentChanged.connect(() => { called = true; });
           let cursor = cell.getMetadata('foo');
@@ -180,7 +175,7 @@ describe('notebook/notebook/model', () => {
         it('should set the dirty flag', () => {
           let model = new NotebookModel();
           let cell = model.factory.createCodeCell();
-          model.cells.add(cell);
+          model.cells.pushBack(cell);
           model.dirty = false;
           cell.source = 'foo';
           expect(model.dirty).to.be(true);

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

@@ -83,7 +83,7 @@ describe('notebook/notebook/modelfactory', () => {
         let factory = new NotebookModelFactory();
         let model = factory.createNew();
         expect(model.cells.length).to.be(1);
-        expect(model.cells.get(0).type).to.be('code');
+        expect(model.cells.at(0).type).to.be('code');
       });
 
       it('should accept a language preference', () => {

+ 3 - 3
test/src/notebook/notebook/trust.spec.ts

@@ -27,7 +27,7 @@ describe('notebook/notebook/trust', () => {
     it('should trust the notebook cells if the user accepts', (done) => {
       let model = new NotebookModel();
       model.fromJSON(DEFAULT_CONTENT);
-      let cell = model.cells.get(0);
+      let cell = model.cells.at(0);
       let cursor = cell.getMetadata('trusted');
       expect(cursor.getValue()).to.not.be(true);
       trustNotebook(model).then(() => {
@@ -40,7 +40,7 @@ describe('notebook/notebook/trust', () => {
     it('should not trust the notebook cells if the user aborts', (done) => {
       let model = new NotebookModel();
       model.fromJSON(DEFAULT_CONTENT);
-      let cell = model.cells.get(0);
+      let cell = model.cells.at(0);
       let cursor = cell.getMetadata('trusted');
       expect(cursor.getValue()).to.not.be(true);
       trustNotebook(model).then(() => {
@@ -58,7 +58,7 @@ describe('notebook/notebook/trust', () => {
       let model = new NotebookModel();
       model.fromJSON(DEFAULT_CONTENT);
       for (let i = 0; i < model.cells.length; i++) {
-        let cell = model.cells.get(i);
+        let cell = model.cells.at(i);
         let cursor = cell.getMetadata('trusted');
         cursor.setValue(true);
       }

+ 12 - 12
test/src/notebook/notebook/widget.spec.ts

@@ -203,7 +203,7 @@ describe('notebook/notebook/widget', () => {
         let called = false;
         widget.modelContentChanged.connect(() => { called = true; });
         let cell = widget.model.factory.createCodeCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         expect(called).to.be(true);
       });
 
@@ -295,7 +295,7 @@ describe('notebook/notebook/widget', () => {
         });
 
         it('should handle a remove', () => {
-          let cell = widget.model.cells.get(1);
+          let cell = widget.model.cells.at(1);
           let child = widget.childAt(1);
           widget.model.cells.remove(cell);
           expect(cell.isDisposed).to.be(true);
@@ -304,7 +304,7 @@ describe('notebook/notebook/widget', () => {
 
         it('should handle an add', () => {
           let cell = widget.model.factory.createCodeCell();
-          widget.model.cells.add(cell);
+          widget.model.cells.pushBack(cell);
           expect(widget.childCount()).to.be(7);
           let child = widget.childAt(0);
           expect(child.hasClass('jp-Notebook-cell')).to.be(true);
@@ -316,10 +316,10 @@ describe('notebook/notebook/widget', () => {
           expect(widget.childAt(2)).to.be(child);
         });
 
-        it('should handle a replace', () => {
+        it('should handle a clear', () => {
           let cell = widget.model.factory.createCodeCell();
-          widget.model.cells.replace(0, 6, [cell]);
-          expect(widget.childCount()).to.be(1);
+          widget.model.cells.clear();
+          expect(widget.childCount()).to.be(0);
         });
 
       });
@@ -479,7 +479,7 @@ describe('notebook/notebook/widget', () => {
 
       it('should be called when a cell is removed', () => {
         let widget = createWidget();
-        let cell = widget.model.cells.get(0);
+        let cell = widget.model.cells.at(0);
         widget.model.cells.remove(cell);
         expect(widget.methods).to.contain('onCellRemoved');
       });
@@ -718,7 +718,7 @@ describe('notebook/notebook/widget', () => {
         Widget.attach(widget, document.body);
         sendMessage(widget, WidgetMessage.ActivateRequest);
         let cell = widget.model.factory.createMarkdownCell();
-        widget.model.cells.add(cell);
+        widget.model.cells.pushBack(cell);
         let child = widget.childAt(widget.childCount() - 1) as MarkdownCellWidget;
         expect(child.rendered).to.be(true);
         widget.activeCellIndex = widget.childCount() - 1;
@@ -913,7 +913,7 @@ describe('notebook/notebook/widget', () => {
 
         it('should preserve "command" mode if in a markdown cell', () => {
           let cell = widget.model.factory.createMarkdownCell();
-          widget.model.cells.add(cell);
+          widget.model.cells.pushBack(cell);
           let count = widget.childCount();
           let child = widget.childAt(count - 1) as MarkdownCellWidget;
           expect(child.rendered).to.be(true);
@@ -928,7 +928,7 @@ describe('notebook/notebook/widget', () => {
 
         it('should unrender a markdown cell', () => {
           let cell = widget.model.factory.createMarkdownCell();
-          widget.model.cells.add(cell);
+          widget.model.cells.pushBack(cell);
           let child = widget.childAt(widget.childCount() - 1) as MarkdownCellWidget;
           expect(child.rendered).to.be(true);
           simulate(child.node, 'dblclick');
@@ -937,7 +937,7 @@ describe('notebook/notebook/widget', () => {
 
         it('should be a no-op if the model is read only', () => {
           let cell = widget.model.factory.createMarkdownCell();
-          widget.model.cells.add(cell);
+          widget.model.cells.pushBack(cell);
           widget.model.readOnly = true;
           let child = widget.childAt(widget.childCount() - 1) as MarkdownCellWidget;
           expect(child.rendered).to.be(true);
@@ -1191,7 +1191,7 @@ describe('notebook/notebook/widget', () => {
 
       it('should post an `update-request', (done) => {
         let widget = createActiveWidget();
-        let cell = widget.model.cells.get(0);
+        let cell = widget.model.cells.at(0);
         widget.model.cells.remove(cell);
         expect(widget.methods).to.contain('onCellRemoved');
         requestAnimationFrame(() => {

+ 2 - 2
test/src/notebook/output-area/model.spec.ts

@@ -92,8 +92,8 @@ describe('notebook/output-area/model', () => {
           expect(args.type).to.be('add');
           expect(args.oldIndex).to.be(-1);
           expect(args.newIndex).to.be(0);
-          expect(args.oldValue).to.be(void 0);
-          expect(deepEqual(args.newValue as nbformat.IOutput, DEFAULT_OUTPUTS[0]));
+          expect(args.oldValues.length).to.be(0);
+          expect(deepEqual(args.newValues.at(0) as nbformat.IOutput, DEFAULT_OUTPUTS[0]));
           called = true;
         });
         model.add(DEFAULT_OUTPUTS[0]);