Explorar o código

[2.2.x] Virtual notebook

Gonzalo Pena-Castellanos %!s(int64=4) %!d(string=hai) anos
pai
achega
371ea88188

+ 8 - 1
packages/cells/src/inputarea.ts

@@ -66,7 +66,9 @@ export class InputArea extends Widget {
 
     const layout = (this.layout = new PanelLayout());
     layout.addWidget(prompt);
-    layout.addWidget(editor);
+    if (!options.placeholder) {
+      layout.addWidget(editor);
+    }
   }
 
   /**
@@ -173,6 +175,11 @@ export namespace InputArea {
      * Whether to send an update request to the editor when it is shown.
      */
     updateOnShow?: boolean;
+
+    /**
+     * Whether this input area is a placeholder for future rendering.
+     */
+    placeholder?: boolean;
   }
 
   /**

+ 44 - 13
packages/cells/src/widget.ts

@@ -196,7 +196,8 @@ export class Cell<T extends ICellModel = ICellModel> extends Widget {
     const input = (this._input = new InputArea({
       model,
       contentFactory,
-      updateOnShow: options.updateEditorOnShow
+      updateOnShow: options.updateEditorOnShow,
+      placeholder: options.placeholder
     }));
     input.addClass(CELL_INPUT_AREA_CLASS);
     inputWrapper.addWidget(inputCollapser);
@@ -452,7 +453,8 @@ export class Cell<T extends ICellModel = ICellModel> extends Widget {
     const constructor = this.constructor as typeof Cell;
     return new constructor({
       model: this.model,
-      contentFactory: this.contentFactory
+      contentFactory: this.contentFactory,
+      placeholder: false
     });
   }
 
@@ -572,6 +574,11 @@ export namespace Cell {
      * The maximum number of output items to display in cell output.
      */
     maxNumberOutputs?: number;
+
+    /**
+     * Whether this cell is a placeholder for future rendering.
+     */
+    placeholder?: boolean;
   }
 
   /**
@@ -700,6 +707,7 @@ export class CodeCell extends Cell<ICodeCellModel> {
     const contentFactory = this.contentFactory;
     const model = this.model;
 
+<<<<<<< HEAD
     // Insert the output before the cell footer.
     const outputWrapper = (this._outputWrapper = new Panel());
     outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS);
@@ -717,15 +725,35 @@ export class CodeCell extends Cell<ICodeCellModel> {
     // if there are no outputs.
     if (model.outputs.length === 0) {
       this.addClass(NO_OUTPUTS_CLASS);
+=======
+    if (!options.placeholder) {
+      // Insert the output before the cell footer.
+      const outputWrapper = (this._outputWrapper = new Panel());
+      outputWrapper.addClass(CELL_OUTPUT_WRAPPER_CLASS);
+      const outputCollapser = new OutputCollapser();
+      outputCollapser.addClass(CELL_OUTPUT_COLLAPSER_CLASS);
+      const output = (this._output = new OutputArea({
+        model: model.outputs,
+        rendermime,
+        contentFactory: contentFactory
+      }));
+      output.addClass(CELL_OUTPUT_AREA_CLASS);
+      // Set a CSS if there are no outputs, and connect a signal for future
+      // changes to the number of outputs. This is for conditional styling
+      // if there are no outputs.
+      if (model.outputs.length === 0) {
+        this.addClass(NO_OUTPUTS_CLASS);
+      }
+      output.outputLengthChanged.connect(this._outputLengthHandler, this);
+      outputWrapper.addWidget(outputCollapser);
+      outputWrapper.addWidget(output);
+      (this.layout as PanelLayout).insertWidget(2, outputWrapper);
+
+      this._outputPlaceholder = new OutputPlaceholder(() => {
+        this.outputHidden = !this.outputHidden;
+      });
+>>>>>>> [2.2.x] Virtual notebook
     }
-    output.outputLengthChanged.connect(this._outputLengthHandler, this);
-    outputWrapper.addWidget(outputCollapser);
-    outputWrapper.addWidget(output);
-    (this.layout as PanelLayout).insertWidget(2, outputWrapper);
-
-    this._outputPlaceholder = new OutputPlaceholder(() => {
-      this.outputHidden = !this.outputHidden;
-    });
     model.stateChanged.connect(this.onStateChanged, this);
   }
 
@@ -916,7 +944,8 @@ export class CodeCell extends Cell<ICodeCellModel> {
     return new constructor({
       model: this.model,
       contentFactory: this.contentFactory,
-      rendermime: this._rendermime
+      rendermime: this._rendermime,
+      placeholder: false
     });
   }
 
@@ -1518,7 +1547,8 @@ export class MarkdownCell extends AttachmentsCell<IMarkdownCellModel> {
     return new constructor({
       model: this.model,
       contentFactory: this.contentFactory,
-      rendermime: this._rendermime
+      rendermime: this._rendermime,
+      placeholder: false
     });
   }
 
@@ -1568,7 +1598,8 @@ export class RawCell extends Cell<IRawCellModel> {
     const constructor = this.constructor as typeof RawCell;
     return new constructor({
       model: this.model,
-      contentFactory: this.contentFactory
+      contentFactory: this.contentFactory,
+      placeholder: false
     });
   }
 }

+ 3 - 2
packages/console/src/widget.ts

@@ -235,7 +235,8 @@ export class CodeConsole extends Widget {
     model.value.text = '...';
     const banner = (this._banner = new RawCell({
       model,
-      contentFactory: this.contentFactory
+      contentFactory: this.contentFactory,
+      placeholder: false
     })).initializeState();
     banner.addClass(BANNER_CLASS);
     banner.readOnly = true;
@@ -731,7 +732,7 @@ export class CodeConsole extends Widget {
     const modelFactory = this.modelFactory;
     const model = modelFactory.createCodeCell({});
     const rendermime = this.rendermime;
-    return { model, rendermime, contentFactory };
+    return { model, rendermime, contentFactory, placeholder: false };
   }
 
   /**

+ 5 - 0
packages/documentsearch/src/searchoverlay.tsx

@@ -43,6 +43,7 @@ const REGEX_ERROR_CLASS = 'jp-DocumentSearch-regex-error';
 const SEARCH_OPTIONS_CLASS = 'jp-DocumentSearch-search-options';
 const SEARCH_OPTIONS_DISABLED_CLASS =
   'jp-DocumentSearch-search-options-disabled';
+const SEARCH_DOCUMENT_LOADING = 'jp-DocumentSearch-document-loading';
 const REPLACE_ENTRY_CLASS = 'jp-DocumentSearch-replace-entry';
 const REPLACE_BUTTON_CLASS = 'jp-DocumentSearch-replace-button';
 const REPLACE_BUTTON_WRAPPER_CLASS = 'jp-DocumentSearch-replace-button-wrapper';
@@ -619,6 +620,10 @@ class SearchOverlay extends React.Component<
         key={3}
       >
         {this.state.errorMessage}
+      </div>,
+      <div className={SEARCH_DOCUMENT_LOADING} key={4}>
+        This document is still loading. Only loaded content will appear in
+        search results until the entire document loads.
       </div>
     ];
   }

+ 10 - 0
packages/documentsearch/style/base.css

@@ -248,3 +248,13 @@
 .jp-DocumentSearch-replace-toggle:hover {
   background-color: var(--jp-layout-color2);
 }
+
+.jp-DocumentSearch-document-loaded .jp-DocumentSearch-document-loading {
+  display: block !important;
+}
+
+.jp-DocumentSearch-document-loading {
+  display: none;
+  margin: 5px;
+  color: var(--jp-ui-font-color2);
+}

+ 18 - 0
packages/notebook-extension/schema/tracker.json

@@ -375,6 +375,24 @@
       "type": "boolean",
       "default": false
     },
+    "numberCellsToRenderDirectly": {
+      "title": "Number of cells to render directly",
+      "description": "Define the number of cells to render directly when virtual notebook intersection observer is available",
+      "type": "number",
+      "default": 7
+    },
+    "renderCellOnIdle": {
+      "title": "Render cell on browser idle time",
+      "description": "Defines if the placeholder cells should be rendered when the browser is idle",
+      "type": "boolean",
+      "default": true
+    },
+    "nonObservedBottomMargin": {
+      "title": "Non-observed bottom margin",
+      "description": "Defines the non-observed bottom margin for the virtual notebook, set a positive number of pixels to render cells below the visible view",
+      "type": "string",
+      "default": "0px"
+    },
     "maxNumberOutputs": {
       "title": "The maximum number of output cells to to be rendered in the output area. Set to 0 to have the complete display.",
       "description": "Defines the maximum number of output cells to to to be rendered in the output area for cells with many outputs. The output area will have a head and a tail, and the outputs between will be trimmed and not displayed unless the user clicks on the information message.",

+ 5 - 0
packages/notebook-extension/src/index.ts

@@ -1028,6 +1028,11 @@ function activateNotebookHandler(
       scrollPastEnd: settings.get('scrollPastEnd').composite as boolean,
       defaultCell: settings.get('defaultCell').composite as nbformat.CellType,
       recordTiming: settings.get('recordTiming').composite as boolean,
+      numberCellsToRenderDirectly: settings.get('numberCellsToRenderDirectly')
+        .composite as number,
+      renderCellOnIdle: settings.get('renderCellOnIdle').composite as boolean,
+      nonObservedBottomMargin: settings.get('nonObservedBottomMargin')
+        .composite as string,
       maxNumberOutputs: settings.get('maxNumberOutputs').composite as number
     };
     factory.shutdownOnClose = settings.get('kernelShutdown')

+ 15 - 0
packages/notebook/src/panel.ts

@@ -37,6 +37,11 @@ const NOTEBOOK_PANEL_TOOLBAR_CLASS = 'jp-NotebookPanel-toolbar';
 
 const NOTEBOOK_PANEL_NOTEBOOK_CLASS = 'jp-NotebookPanel-notebook';
 
+/**
+ * The class name to add when the document is loaded for the search box.
+ */
+const SEARCH_DOCUMENT_LOADED_CLASS = 'jp-DocumentSearch-document-loaded';
+
 /**
  * A widget that hosts a notebook toolbar and content area.
  *
@@ -68,6 +73,7 @@ export class NotebookPanel extends DocumentWidget<Notebook, INotebookModel> {
       this._onSessionStatusChanged,
       this
     );
+    this.content.fullyRendered.connect(this._onFullyRendered, this);
     this.context.saveState.connect(this._onSave, this);
     void this.revealed.then(() => {
       if (this.isDisposed) {
@@ -168,6 +174,15 @@ export class NotebookPanel extends DocumentWidget<Notebook, INotebookModel> {
     };
   }
 
+  /**
+   * Handle a fully rendered signal notebook.
+   */
+  private _onFullyRendered(notebook: Notebook, fullyRendered: boolean): void {
+    fullyRendered
+      ? this.removeClass(SEARCH_DOCUMENT_LOADED_CLASS)
+      : this.addClass(SEARCH_DOCUMENT_LOADED_CLASS);
+  }
+
   /**
    * Handle a change in the kernel by updating the document metadata.
    */

+ 189 - 13
packages/notebook/src/widget.ts

@@ -144,6 +144,11 @@ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
  */
 const DRAG_THRESHOLD = 5;
 
+/*
+ * The type of cell insert provided via signal.
+ */
+type InsertType = 'push' | 'insert' | 'set';
+
 /**
  * The interactivity modes for the notebook.
  */
@@ -152,19 +157,19 @@ export type NotebookMode = 'command' | 'edit';
 if ((window as any).requestIdleCallback === undefined) {
   // On Safari, requestIdleCallback is not available, so we use replacement functions for `idleCallbacks`
   // See: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#falling_back_to_settimeout
-  (window as any).requestIdleCallback = function(handler: Function) {
+  (window as any).requestIdleCallback = function (handler: Function) {
     let startTime = Date.now();
-    return setTimeout(function() {
+    return setTimeout(function () {
       handler({
         didTimeout: false,
-        timeRemaining: function() {
+        timeRemaining: function () {
           return Math.max(0, 50.0 - (Date.now() - startTime));
         }
       });
     }, 1);
   };
 
-  (window as any).cancelIdleCallback = function(id: number) {
+  (window as any).cancelIdleCallback = function (id: number) {
     clearTimeout(id);
   };
 }
@@ -196,6 +201,46 @@ export class StaticNotebook extends Widget {
     this.notebookConfig =
       options.notebookConfig || StaticNotebook.defaultNotebookConfig;
     this._mimetypeService = options.mimeTypeService;
+
+    // Section for the virtual-notebook behavior.
+    this._toRenderMap = new Map<string, { index: number; cell: Cell }>();
+    this._cellsArray = new Array<Cell>();
+    if ('IntersectionObserver' in window) {
+      this._observer = new IntersectionObserver(
+        (entries, observer) => {
+          entries.forEach(o => {
+            if (o.isIntersecting) {
+              observer.unobserve(o.target);
+              const ci = this._toRenderMap.get(o.target.id);
+              if (ci) {
+                const { cell, index } = ci;
+                this._renderPlaceholderCell(cell, index);
+              }
+            }
+          });
+        },
+        {
+          root: this.node,
+          threshold: 1,
+          rootMargin:
+            '0px 0px ' + this.notebookConfig.nonObservedBottomMargin + ' 0px'
+        }
+      );
+    }
+  }
+
+  /**
+   * A signal emitted when the notebook is fully rendered.
+   */
+  get fullyRendered(): ISignal<this, boolean> {
+    return this._fullyRendered;
+  }
+
+  /**
+   * A signal emitted when the a placeholder cell is rendered.
+   */
+  get placeholderCellRendered(): ISignal<this, Cell> {
+    return this._placeholderCellRendered;
   }
 
   /**
@@ -415,7 +460,7 @@ export class StaticNotebook extends Widget {
     }
 
     each(cells, (cell: ICellModel, i: number) => {
-      this._insertCell(i, cell);
+      this._insertCell(i, cell, 'set');
     });
     cells.changed.connect(this._onCellsChanged, this);
     newValue.contentChanged.connect(this.onModelContentChanged, this);
@@ -433,8 +478,10 @@ export class StaticNotebook extends Widget {
     switch (args.type) {
       case 'add':
         index = args.newIndex;
+        // eslint-disable-next-line no-case-declarations
+        const insertType: InsertType = args.oldIndex == -1 ? 'push' : 'insert';
         each(args.newValues, value => {
-          this._insertCell(index++, value);
+          this._insertCell(index++, value, insertType);
         });
         break;
       case 'move':
@@ -468,7 +515,7 @@ export class StaticNotebook extends Widget {
           // Note: this ordering (insert then remove)
           // is important for getting the active cell
           // index for the editable notebook correct.
-          this._insertCell(index, value);
+          this._insertCell(index, value, 'set');
           this._removeCell(index + 1);
           index++;
         });
@@ -481,7 +528,11 @@ export class StaticNotebook extends Widget {
   /**
    * Create a cell widget and insert into the notebook.
    */
-  private _insertCell(index: number, cell: ICellModel): void {
+  private _insertCell(
+    index: number,
+    cell: ICellModel,
+    insertType: InsertType
+  ): void {
     let widget: Cell;
     switch (cell.type) {
       case 'code':
@@ -498,9 +549,63 @@ export class StaticNotebook extends Widget {
         widget = this._createRawCell(cell as IRawCellModel);
     }
     widget.addClass(NB_CELL_CLASS);
+
     const layout = this.layout as PanelLayout;
-    layout.insertWidget(index, widget);
-    this.onCellInserted(index, widget);
+    this._cellsArray.push(widget);
+    if (
+      this._observer &&
+      insertType === 'push' &&
+      this._renderedCellsCount > this.notebookConfig.numberCellsToRenderDirectly
+    ) {
+      // We have an observer and we are have been asked to push (not to insert).
+      // and we are above the number of cells to render directly, then
+      // we will add a placeholder and let the instersection observer or the
+      // idle browser render those placeholder cells.
+      this._toRenderMap.set(widget.model.id, { index: index, cell: widget });
+      const placeholder = this._createPlaceholderCell(
+        cell as IRawCellModel,
+        index
+      );
+      placeholder.node.id = widget.model.id;
+      layout.insertWidget(index, placeholder);
+      this.onCellInserted(index, placeholder);
+      this._fullyRendered.emit(false);
+      this._observer.observe(placeholder.node);
+    } else {
+      // We have no intersection observer, or we insert, or we are below
+      // the number of cells to render directly, so we render directly.
+      layout.insertWidget(index, widget);
+      this._incrementRenderedCount();
+      this.onCellInserted(index, widget);
+    }
+
+    if (this._observer && this.notebookConfig.renderCellOnIdle) {
+      const renderPlaceholderCells = this._renderPlaceholderCells.bind(this);
+      (window as any).requestIdleCallback(renderPlaceholderCells, {
+        timeout: 1000
+      });
+    }
+  }
+
+  private _renderPlaceholderCells(deadline: any) {
+    if (
+      this._renderedCellsCount < this._cellsArray.length &&
+      this._renderedCellsCount >=
+        this.notebookConfig.numberCellsToRenderDirectly
+    ) {
+      const index = this._renderedCellsCount;
+      const cell = this._cellsArray[index];
+      this._renderPlaceholderCell(cell, index - 1);
+    }
+  }
+
+  private _renderPlaceholderCell(cell: Cell, index: number) {
+    const pl = this.layout as PanelLayout;
+    pl.removeWidgetAt(index);
+    pl.insertWidget(index, cell);
+    this._toRenderMap.delete(cell.model.id);
+    this._incrementRenderedCount();
+    this._placeholderCellRendered.emit(cell);
   }
 
   /**
@@ -538,7 +643,8 @@ export class StaticNotebook extends Widget {
       model,
       rendermime,
       contentFactory,
-      updateEditorOnShow: false
+      updateEditorOnShow: false,
+      placeholder: false
     };
     const cell = this.contentFactory.createMarkdownCell(options, this);
     cell.syncCollapse = true;
@@ -546,6 +652,40 @@ export class StaticNotebook extends Widget {
     return cell;
   }
 
+  /**
+   * Create a placeholder cell widget from a raw cell model.
+   */
+  private _createPlaceholderCell(model: IRawCellModel, index: number): RawCell {
+    const contentFactory = this.contentFactory;
+    const editorConfig = this.editorConfig.raw;
+    const options = {
+      editorConfig,
+      model,
+      contentFactory,
+      updateEditorOnShow: false,
+      placeholder: true
+    };
+    const cell = this.contentFactory.createRawCell(options, this);
+    cell.node.innerHTML = `
+      <div class="jp-Cell-Placeholder">
+        <div class="jp-Cell-Placeholder-wrapper">
+          <div class="jp-Cell-Placeholder-wrapper-inner">
+            <div class="jp-Cell-Placeholder-wrapper-body">
+              <div class="jp-Cell-Placeholder-h1"></div>
+              <div class="jp-Cell-Placeholder-h2"></div>
+              <div class="jp-Cell-Placeholder-content-1"></div>
+              <div class="jp-Cell-Placeholder-content-2"></div>
+              <div class="jp-Cell-Placeholder-content-3"></div>
+            </div>
+          </div>
+        </div>
+      </div>`;
+    cell.inputHidden = true;
+    cell.syncCollapse = true;
+    cell.syncEditable = true;
+    return cell;
+  }
+
   /**
    * Create a raw cell widget from a raw cell model.
    */
@@ -556,7 +696,8 @@ export class StaticNotebook extends Widget {
       editorConfig,
       model,
       contentFactory,
-      updateEditorOnShow: false
+      updateEditorOnShow: false,
+      placeholder: false
     };
     const cell = this.contentFactory.createRawCell(options, this);
     cell.syncCollapse = true;
@@ -656,6 +797,13 @@ export class StaticNotebook extends Widget {
     );
   }
 
+  private _incrementRenderedCount() {
+    if (this._toRenderMap.size === 0) {
+      this._fullyRendered.emit(true);
+    }
+    this._renderedCellsCount++;
+  }
+
   private _editorConfig = StaticNotebook.defaultEditorConfig;
   private _notebookConfig = StaticNotebook.defaultNotebookConfig;
   private _mimetype = 'text/plain';
@@ -663,6 +811,12 @@ export class StaticNotebook extends Widget {
   private _mimetypeService: IEditorMimeTypeService;
   private _modelChanged = new Signal<this, void>(this);
   private _modelContentChanged = new Signal<this, void>(this);
+  private _fullyRendered = new Signal<this, boolean>(this);
+  private _placeholderCellRendered = new Signal<this, Cell>(this);
+  private _observer: IntersectionObserver;
+  private _renderedCellsCount = 0;
+  private _toRenderMap: Map<string, { index: number; cell: Cell }>;
+  private _cellsArray: Array<Cell>;
 }
 
 /**
@@ -796,6 +950,25 @@ export namespace StaticNotebook {
      */
     recordTiming: boolean;
 
+    /*
+     * Number of cells to render directly when virtual
+     * notebook intersection observer is available.
+     */
+    numberCellsToRenderDirectly: number;
+
+    /**
+     * Defines if the placeholder cells should be rendered
+     * when the browser is idle.
+     */
+    renderCellOnIdle: boolean;
+
+    /**
+     * Defines the non-observed bottom margin for the
+     * virtual notebook, set a positive number of pixels
+     * to render cells below the visible view.
+     */
+    nonObservedBottomMargin: string;
+
     /**
      * Defines the maximum number of outputs per cell.
      */
@@ -808,7 +981,10 @@ export namespace StaticNotebook {
     scrollPastEnd: true,
     defaultCell: 'code',
     recordTiming: false,
-    maxNumberOutputs: 50
+    maxNumberOutputs: 50,
+    numberCellsToRenderDirectly: 10,
+    renderCellOnIdle: true,
+    nonObservedBottomMargin: '0px'
   };
 
   /**

+ 85 - 0
packages/notebook/style/base.css

@@ -271,3 +271,88 @@
 .jp-mod-presentationMode .jp-Notebook .jp-Cell .jp-OutputPrompt {
   flex: 0 0 110px;
 }
+
+/*-----------------------------------------------------------------------------
+| Placeholder
+|----------------------------------------------------------------------------*/
+
+.jp-Cell-Placeholder {
+  height: 200px !important;
+  padding-left: 55px;
+}
+
+.jp-Cell-Placeholder-wrapper {
+  background: #fff;
+  border: 1px solid;
+  border-color: #e5e6e9 #dfe0e4 #d0d1d5;
+  border-radius: 4px;
+  -webkit-border-radius: 4px;
+  margin: 10px 15px;
+}
+
+.jp-Cell-Placeholder-wrapper-inner {
+  height: 150px;
+  padding: 15px;
+  position: relative;
+}
+
+.jp-Cell-Placeholder-wrapper-body {
+  background-repeat: repeat;
+  background-size: 50% auto;
+}
+
+.jp-Cell-Placeholder-wrapper-body div {
+  background: #f6f7f8;
+  background-image: -webkit-linear-gradient(
+    left,
+    #f6f7f8 0%,
+    #edeef1 20%,
+    #f6f7f8 40%,
+    #f6f7f8 100%
+  );
+  background-repeat: no-repeat;
+  background-size: 800px 104px;
+  height: 104px;
+  position: relative;
+}
+
+.jp-Cell-Placeholder-wrapper-body div {
+  position: absolute;
+  right: 15px;
+  left: 15px;
+  top: 15px;
+}
+
+div.jp-Cell-Placeholder-h1 {
+  top: 20px;
+  height: 20px;
+  left: 15px;
+  width: 150px;
+}
+
+div.jp-Cell-Placeholder-h2 {
+  left: 15px;
+  top: 50px;
+  height: 10px;
+  width: 100px;
+}
+
+div.jp-Cell-Placeholder-content-1,
+div.jp-Cell-Placeholder-content-2,
+div.jp-Cell-Placeholder-content-3 {
+  left: 15px;
+  right: 15px;
+  height: 10px;
+}
+
+div.jp-Cell-Placeholder-content-1 {
+  top: 100px;
+}
+
+div.jp-Cell-Placeholder-content-2 {
+  top: 120px;
+}
+
+div.jp-Cell-Placeholder-content-3 {
+  top: 140px;
+}

+ 14 - 2
packages/observables/src/observablelist.ts

@@ -433,6 +433,10 @@ export class ObservableList<T> implements IObservableList<T> {
    * #### Complexity
    * Constant.
    *
+   * #### Notes
+   * By convention, the oldIndex is set to -1 to indicate
+   * an push operation.
+   *
    * #### Iterator Validity
    * No changes.
    */
@@ -463,6 +467,8 @@ export class ObservableList<T> implements IObservableList<T> {
    *
    * #### Notes
    * The `index` will be clamped to the bounds of the list.
+   * By convention, the oldIndex is set to -2 to indicate
+   * an insert operation.
    *
    * #### Undefined Behavior
    * An `index` which is non-integral.
@@ -471,7 +477,7 @@ export class ObservableList<T> implements IObservableList<T> {
     ArrayExt.insert(this._array, index, value);
     this._changed.emit({
       type: 'add',
-      oldIndex: -1,
+      oldIndex: -2,
       newIndex: index,
       oldValues: [],
       newValues: [value]
@@ -596,6 +602,10 @@ export class ObservableList<T> implements IObservableList<T> {
    * #### Complexity
    * Linear.
    *
+   * #### Notes
+   * By convention, the oldIndex is set to -1 to indicate
+   * an push operation.
+   *
    * #### Iterator Validity
    * No changes.
    */
@@ -629,6 +639,8 @@ export class ObservableList<T> implements IObservableList<T> {
    *
    * #### Notes
    * The `index` will be clamped to the bounds of the list.
+   * By convention, the oldIndex is set to -2 to indicate
+   * an insert operation.
    *
    * #### Undefined Behavior.
    * An `index` which is non-integral.
@@ -640,7 +652,7 @@ export class ObservableList<T> implements IObservableList<T> {
     });
     this._changed.emit({
       type: 'add',
-      oldIndex: -1,
+      oldIndex: -2,
       newIndex,
       oldValues: [],
       newValues: toArray(values)

+ 2 - 2
packages/observables/test/observablelist.spec.ts

@@ -146,7 +146,7 @@ describe('@jupyterlab/observables', () => {
           expect(sender).toBe(value);
           expect(args.type).toBe('add');
           expect(args.newIndex).toBe(1);
-          expect(args.oldIndex).toBe(-1);
+          expect(args.oldIndex).toBe(-2);
           expect(args.oldValues.length).toBe(0);
           expect(args.newValues[0]).toBe(4);
           called = true;
@@ -324,7 +324,7 @@ describe('@jupyterlab/observables', () => {
           expect(sender).toBe(value);
           expect(args.type).toBe('add');
           expect(args.newIndex).toBe(1);
-          expect(args.oldIndex).toBe(-1);
+          expect(args.oldIndex).toBe(-2);
           expect(toArray(args.newValues)).toEqual([4, 5, 6]);
           expect(args.oldValues.length).toBe(0);
           called = true;