Browse Source

Finish implementation

Steven Silvester 8 years ago
parent
commit
abe427923f

+ 1 - 1
examples/notebook/package.json

@@ -20,7 +20,7 @@
   "dependencies": {
     "jupyter-js-notebook": "file:../..",
     "jupyter-js-services": "^0.10.1",
-    "jupyter-js-ui": "^0.12.1",
+    "jupyter-js-ui": "file:../../../ui",
     "phosphor-commandpalette": "^0.2.0",
     "phosphor-keymap": "^0.8.0",
     "phosphor-splitpanel": "^1.0.0-rc.1",

+ 54 - 17
examples/notebook/src/index.ts

@@ -39,10 +39,6 @@ import {
   KeymapManager
 } from 'phosphor-keymap';
 
-import {
-  PanelLayout
-} from 'phosphor-panel';
-
 import {
   SplitPanel
 } from 'phosphor-splitpanel';
@@ -97,14 +93,9 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   }
   let rendermime = new RenderMime<Widget>(renderers, order);
 
-  let nbWidget: NotebookPanel;
-
   let opener = {
     open: (widget: DocumentWidget) => {
-      panel.addChild(widget);
-      SplitPanel.setStretch(widget, 1);
-      let layout = widget.layout as PanelLayout;
-      nbWidget = layout.childAt(0) as NotebookPanel;
+      // Do nothing for sibling widgets for now.
     }
   };
 
@@ -115,7 +106,7 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   let wFactory = new NotebookWidgetFactory(rendermime, clipboard);
   docManager.registerModelFactory(mFactory, {
     name: 'notebook',
-    contentsOptions: { format: 'json', type: 'notebook' }
+    contentsOptions: { type: 'notebook' }
   });
   docManager.registerWidgetFactory(wFactory, {
     displayName: 'Notebook',
@@ -125,7 +116,11 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
     preferKernel: true,
     canStartKernel: true
   });
-  docManager.open(NOTEBOOK);
+  let doc = docManager.open(NOTEBOOK);
+  let nbWidget: NotebookPanel;
+  doc.populated.connect((d, widget) => {
+    nbWidget = widget as NotebookPanel;
+  });
 
   let pModel = new StandardPaletteModel();
   let palette = new CommandPalette();
@@ -138,6 +133,8 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   SplitPanel.setStretch(palette, 0);
   panel.attach(document.body);
   panel.addChild(palette);
+  panel.addChild(doc);
+  SplitPanel.setStretch(doc, 1);
   window.onresize = () => { panel.update(); };
 
   let saveHandler = () => { nbWidget.context.save(); };
@@ -146,10 +143,17 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
       nbWidget.context.kernel.interrupt();
     }
   };
-  let restartHandler = () => {
-    if (nbWidget.context.kernel) {
-      nbWidget.context.kernel.restart();
+  let restartHandler = () => {  nbWidget.restart(); };
+  let switchHandler = () => {
+    let context = nbWidget.context;
+    if (!context.kernel) {
+      return;
     }
+    selectKernel(nbWidget.node, context.kernel.name, specs).then(name => {
+      if (name) {
+        context.changeKernel({ name });
+      }
+    });
   };
   let runAdvanceHandler = () => {
     NotebookActions.runAndAdvance(nbWidget.content, nbWidget.context.kernel);
@@ -201,6 +205,12 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   let splitHandler = () => {
     NotebookActions.splitCell(nbWidget.content);
   };
+  let undoHandler = () => {
+    nbWidget.model.undo();
+  };
+  let redoHandler = () => {
+    nbWidget.model.redo();
+  };
 
   let items: IStandardPaletteItemOptions[] = [
   {
@@ -221,6 +231,11 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
     shortcut: '0 0',
     handler: restartHandler
   },
+  {
+    category: 'Notebook',
+    text: 'Switch Kernel',
+    handler: switchHandler
+  },
   {
     category: 'Notebook',
     text: 'Trust Notebook',
@@ -320,7 +335,7 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   {
     category: 'Notebook Cell',
     text: 'To Code Type',
-    shortcut: 'Y',
+    shortcut: 'E',
     handler: codeHandler
   },
   {
@@ -347,6 +362,18 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
     shortcut: 'ArrowDown',
     handler: selectBelowHandler
   },
+  {
+    category: 'Notebook Cell',
+    text: 'Undo Cell Action',
+    shortcut: 'Z',
+    handler: undoHandler
+  },
+  {
+    category: 'Notebook Cell',
+    text: 'Redo Cell Action',
+    shortcut: 'Y',
+    handler: redoHandler
+  },
   ];
   pModel.addItems(items);
 
@@ -383,7 +410,7 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
-    sequence: ['Y'],
+    sequence: ['E'],
     handler: codeHandler
   },
   {
@@ -465,6 +492,16 @@ function createApp(sessionsManager: NotebookSessionManager, specs: IKernelSpecId
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['Shift J'],
     handler: extendBelowHandler
+  },
+  {
+    selector: '.jp-Notebook.jp-mod-commandMode',
+    sequence: ['Z'],
+    handler: undoHandler
+  },
+    {
+    selector: '.jp-Notebook.jp-mod-commandMode',
+    sequence: ['Y'],
+    handler: redoHandler
   }
   ];
   keymap.add(bindings);

+ 1 - 7
examples/notebook/test.ipynb

@@ -4,7 +4,6 @@
    "cell_type": "code",
    "execution_count": 1,
    "metadata": {
-    "collapsed": false,
     "tags": []
    },
    "outputs": [
@@ -59,7 +58,6 @@
    "cell_type": "code",
    "execution_count": 2,
    "metadata": {
-    "collapsed": false,
     "tags": []
    },
    "outputs": [
@@ -80,7 +78,6 @@
    "cell_type": "code",
    "execution_count": 3,
    "metadata": {
-    "collapsed": false,
     "tags": []
    },
    "outputs": [
@@ -105,7 +102,6 @@
    "cell_type": "code",
    "execution_count": 4,
    "metadata": {
-    "collapsed": false,
     "tags": []
    },
    "outputs": [
@@ -149,9 +145,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": []
   }

+ 1 - 1
package.json

@@ -9,7 +9,7 @@
     "diff-match-patch": "^1.0.0",
     "file-loader": "^0.8.5",
     "jupyter-js-services": "^0.10.1",
-    "jupyter-js-ui": "^0.12.0",
+    "jupyter-js-ui": "file:../ui",
     "jupyter-js-utils": "^0.4.0",
     "marked": "^0.3.5",
     "phosphor-disposable": "^1.0.5",

+ 8 - 7
src/notebook/cells/model.ts

@@ -201,7 +201,7 @@ class CellModel implements ICellModel {
   /**
    * Set the cursor data for a given field.
    */
-  protected setCursorData(name: string, value: string): void {
+  protected setCursorData(name: string, value: any): void {
     if (this._metadata[name] === value) {
       return;
     }
@@ -214,7 +214,7 @@ class CellModel implements ICellModel {
    */
   type: CellType;
 
-  private _metadata: { [key: string]: string } = Object.create(null);
+  private _metadata: { [key: string]: any } = Object.create(null);
   private _cursors: MetadataCursor[] = [];
   private _source = '';
 }
@@ -352,7 +352,7 @@ class MetadataCursor implements IMetadataCursor {
    *
    * @param cb - a change callback.
    */
-  constructor(name: string, read: () => string, write: (value: string) => void) {
+  constructor(name: string, read: () => any, write: (value: any) => void) {
     this._name = name;
     this._read = read;
     this._write = write;
@@ -390,15 +390,16 @@ class MetadataCursor implements IMetadataCursor {
    * Get the value of the namespace data.
    */
   getValue(): any {
-    let value = this._read.call(void 0);
-    return JSON.parse(value || 'null');
+    let read = this._read;
+    return read();
   }
 
   /**
    * Set the value of the namespace data.
    */
-  setValue(value: any): any {
-    this._write.call(void 0, JSON.stringify(value));
+  setValue(value: any): void {
+    let write = this._write;
+    write(value);
   }
 
   private _name = '';

+ 22 - 6
src/notebook/cells/widget.ts

@@ -2,6 +2,9 @@
 // Distributed under the terms of the Modified BSD License.
 'use strict';
 
+import * as CodeMirror
+  from 'codemirror';
+
 import {
   loadModeByMIME
 } from 'jupyter-js-ui/lib/codemirror';
@@ -63,6 +66,11 @@ const PROMPT_CLASS = 'jp-InputArea-prompt';
  */
 const EDITOR_CLASS = 'jp-InputArea-editor';
 
+/**
+ * The class name added to a codemirror widget.
+ */
+const CODEMIRROR_CLASS = 'jp-CodeMirror';
+
 /**
  * The class name added to the cell when collapsed.
  */
@@ -124,6 +132,7 @@ class BaseCellWidget extends Widget {
     model.contentChanged.connect(this.onModelChanged, this);
     this._trustedCursor = model.getMetadata('trusted');
     this._trusted = this._trustedCursor.getValue();
+    this.editor.getDoc().setValue(model.source);
   }
 
   /**
@@ -258,7 +267,11 @@ class BaseCellWidget extends Widget {
   protected onModelChanged(model: ICellModel, change: string): void {
     switch (change) {
     case 'source':
-      this.editor.getDoc().setValue(model.source);
+      let doc = this.editor.getDoc();
+      let value = doc.getValue();
+      if (value !== model.source) {
+        doc.setValue(model.source);
+      }
       break;
     case 'metadata':
     case 'metadata.trusted':
@@ -319,8 +332,7 @@ class CodeCellWidget extends BaseCellWidget {
     (this.layout as PanelLayout).addChild(this._output);
     this._collapsedCursor = model.getMetadata('collapsed');
     this._scrolledCursor = model.getMetadata('scrolled');
-    let value = model.executionCount;
-    this.setPrompt(`In [${value || ' '}]:`);
+    this.setPrompt(String(model.executionCount));
   }
 
   /**
@@ -359,8 +371,7 @@ class CodeCellWidget extends BaseCellWidget {
       this.update();
       break;
     case 'executionCount':
-      let value = model.executionCount;
-      this.setPrompt(`In [${value || ' '}]:`);
+      this.setPrompt(String(model.executionCount));
       break;
     default:
       break;
@@ -487,6 +498,7 @@ class InputAreaWidget extends Widget {
     super();
     this.addClass(INPUT_CLASS);
     editor.addClass(EDITOR_CLASS);
+    editor.addClass(CODEMIRROR_CLASS);
     this.layout = new PanelLayout();
     let prompt = new Widget();
     prompt.addClass(PROMPT_CLASS);
@@ -500,6 +512,10 @@ class InputAreaWidget extends Widget {
    */
   setPrompt(value: string): void {
     let prompt = (this.layout as PanelLayout).childAt(0);
-    prompt.node.textContent = value;
+    if (value === 'null') {
+      value = ' ';
+    }
+    let text = `In [${value || ' '}]:`;
+    prompt.node.textContent = text;
   }
 }

+ 1 - 1
src/notebook/kernel-selector/index.ts

@@ -3,7 +3,7 @@
 'use strict';
 
 import {
-  IKernelSpecIds, IKernel
+  IKernelSpecIds
 } from 'jupyter-js-services';
 
 import {

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

@@ -66,6 +66,7 @@ namespace NotebookActions {
 
     // Make the changes while preserving history.
     nbModel.cells.replace(index, 1, [clone0, clone1]);
+    widget.activeCellIndex++;
   }
 
   /**
@@ -249,6 +250,7 @@ namespace NotebookActions {
       widget.mode = 'edit';
     } else {
       widget.activeCellIndex++;
+      widget.mode = 'command';
     }
     Private.deselectCells(widget);
   }
@@ -262,6 +264,7 @@ namespace NotebookActions {
     let model = widget.model;
     let cell = model.createCodeCell();
     model.cells.insert(widget.activeCellIndex + 1, cell);
+    widget.activeCellIndex++;
     widget.mode = 'edit';
     Private.deselectCells(widget);
   }
@@ -275,6 +278,7 @@ namespace NotebookActions {
       return;
     }
     widget.activeCellIndex += 1;
+    widget.mode = 'command';
     Private.deselectCells(widget);
   }
 
@@ -287,6 +291,7 @@ namespace NotebookActions {
       return;
     }
     widget.activeCellIndex -= 1;
+    widget.mode = 'command';
     Private.deselectCells(widget);
   }
 

+ 2 - 12
src/notebook/notebook/default-toolbar.ts

@@ -180,17 +180,7 @@ namespace ToolbarItems {
   export
   function createRestartButton(panel: NotebookPanel): ToolbarButton {
     return new ToolbarButton(TOOLBAR_RESTART, () => {
-      if (panel.context.kernel) {
-        showDialog({
-          title: 'Restart Kernel?',
-          body: 'Do you want to restart the current kernel? All variables will be lost.',
-          host: panel.node
-        }).then(result => {
-          if (result.text === 'OK') {
-            panel.context.kernel.restart();
-          }
-        });
-      }
+      panel.restart();
     }, 'Restart the kernel');
   }
 
@@ -329,7 +319,7 @@ class KernelIndicator extends Widget {
    * Handle a status on a kernel.
    */
   private _handleStatus(kernel: IKernel, status: KernelStatus) {
-    this.toggleClass(TOOLBAR_BUSY, status === KernelStatus.Idle);
+    this.toggleClass(TOOLBAR_BUSY, status !== KernelStatus.Idle);
     switch (status) {
     case KernelStatus.Idle:
       this.node.title = 'Kernel Idle';

+ 11 - 9
src/notebook/notebook/model.ts

@@ -252,7 +252,7 @@ class NotebookModel implements INotebookModel {
    * This is a read-only property.
    */
   get defaultKernelName(): string {
-    let spec = JSON.parse(this._metadata['kernelspec']);
+    let spec = this._metadata['kernelspec'];
     return spec ? spec.name : '';
   }
 
@@ -263,7 +263,7 @@ class NotebookModel implements INotebookModel {
    * This is a read-only property.
    */
   get defaultKernelLanguage(): string {
-    let info = JSON.parse(this._metadata['language_info']);
+    let info = this._metadata['language_info'];
     return info ? info.name : '';
   }
 
@@ -387,6 +387,13 @@ class NotebookModel implements INotebookModel {
     this.contentChanged.emit('metadata');
   }
 
+  /**
+   * Initialize the model state.
+   */
+  initialize(): void {
+    this._changeStack.clear();
+  }
+
   /**
    * A factory for creating a new code cell.
    *
@@ -495,20 +502,15 @@ class NotebookModel implements INotebookModel {
       cell.contentChanged.connect(this.onCellChanged, this);
       break;
     case ListChangeType.Remove:
-      (change.oldValue as ICellModel).dispose();
+      // Handled by undo.
       break;
     case ListChangeType.Replace:
-      let oldValues = change.oldValue as ICellModel[];
-      for (cell of oldValues) {
-        cell.dispose();
-      }
       let newValues = change.newValue as ICellModel[];
       for (cell of newValues) {
         cell.contentChanged.connect(this.onCellChanged, this);
       }
       break;
     case ListChangeType.Set:
-      (change.oldValue as ICellModel).dispose();
       cell = change.newValue as ICellModel;
       cell.contentChanged.connect(this.onCellChanged, this);
       break;
@@ -537,7 +539,7 @@ class NotebookModel implements INotebookModel {
   }
 
   private _cells: IObservableList<ICellModel> = null;
-  private _metadata: { [key: string]: string } = Object.create(null);
+  private _metadata: { [key: string]: any } = Object.create(null);
   private _dirty = false;
   private _readOnly = false;
   private _cursors: MetadataCursor[] = [];

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

@@ -38,7 +38,10 @@ class NotebookModelFactory implements IModelFactory {
    * @returns A new document model.
    */
   createNew(languagePreference?: string): INotebookModel {
-    return new NotebookModel(languagePreference);
+    let model = new NotebookModel(languagePreference);
+    let cell = model.createCodeCell();
+    model.cells.add(cell);
+    return model;
   }
 
   /**

+ 23 - 0
src/notebook/notebook/panel.ts

@@ -6,6 +6,10 @@ import {
   IKernel
 } from 'jupyter-js-services';
 
+import {
+  showDialog
+} from 'jupyter-js-ui/lib/dialog';
+
 import {
   IDocumentContext
 } from 'jupyter-js-ui/lib/docmanager';
@@ -177,6 +181,25 @@ class NotebookPanel extends Widget {
     super.dispose();
   }
 
+  /**
+   * Restart the kernel on the panel.
+   */
+  restart(): void {
+    let kernel = this.context.kernel;
+    if (!kernel) {
+      return;
+    }
+    showDialog({
+      title: 'Restart Kernel?',
+      body: 'Do you want to restart the current kernel? All variables will be lost.',
+      host: this.node
+    }).then(result => {
+      if (result.text === 'OK') {
+        kernel.restart();
+      }
+    });
+  }
+
   /**
    * Handle a change in the kernel by updating the document metadata.
    */

+ 1 - 0
src/notebook/notebook/toolbar.ts

@@ -80,6 +80,7 @@ class NotebookToolbar extends Widget {
     } else {
       layout.insertChild(index, widget);
     }
+    Private.nameProperty.set(widget, name);
   }
 
   /**

+ 16 - 4
src/notebook/notebook/undo.ts

@@ -79,7 +79,7 @@ class NotebookUndo implements IDisposable {
    */
   beginCompoundOperation(isUndoAble?: boolean): void {
     this._inCompound = true;
-    this._isUndoable = isUndoAble === true;
+    this._isUndoable = (isUndoAble !== false);
     this._madeCompoundChange = false;
   }
 
@@ -126,6 +126,14 @@ class NotebookUndo implements IDisposable {
     this._isUndoable = true;
   }
 
+  /**
+   * Clear the change stack.
+   */
+  clear(): void {
+    this._index = -1;
+    this._stack = [];
+  }
+
   /**
    * Handle a change in the cells list.
    */
@@ -138,8 +146,8 @@ class NotebookUndo implements IDisposable {
     // Copy the change.
     let evt = this._copyChange(change);
     // Put the change in the stack.
-    if (this._stack[this._index]) {
-      this._stack[this._index].push(evt);
+    if (this._stack[this._index + 1]) {
+      this._stack[this._index + 1].push(evt);
     } else {
       this._stack.push([evt]);
     }
@@ -198,7 +206,7 @@ class NotebookUndo implements IDisposable {
       list.set(change.newIndex, cell);
       break;
     case ListChangeType.Remove:
-      list.removeAt(change.newIndex);
+      list.removeAt(change.oldIndex);
       break;
     case ListChangeType.Move:
       list.move(change.oldIndex, change.newIndex);
@@ -264,6 +272,9 @@ class NotebookUndo implements IDisposable {
     default:
       return;
     }
+    if (oldValue) {
+      (change.oldValue as ICellModel).dispose();
+    }
     return {
       type: change.type,
       oldIndex: change.oldIndex,
@@ -280,6 +291,7 @@ class NotebookUndo implements IDisposable {
     let oldValue: IBaseCell[] = [];
     for (let cell of (change.oldValue as ICellModel[])) {
       oldValue.push(cell.toJSON());
+      cell.dispose();
     }
     let newValue: IBaseCell[] = [];
     for (let cell of (change.newValue as ICellModel[])) {

+ 8 - 8
src/notebook/notebook/widget.ts

@@ -253,8 +253,8 @@ class NotebookRenderer extends Widget {
         widget.dispose();
       }
       let newValues = args.newValue as ICellModel[];
-      for (let i = newValues.length; i < 0; i--) {
-        widget = factory(newValues[i], this._rendermime);
+      for (let i = newValues.length; i > 0; i--) {
+        widget = factory(newValues[i - 1], this._rendermime);
         this._initializeCellWidget(widget);
         layout.insertChild(args.newIndex, widget);
       }
@@ -431,6 +431,7 @@ class ActiveNotebook extends NotebookRenderer {
   protected onAfterAttach(msg: Message): void {
     this.node.addEventListener('click', this);
     this.node.addEventListener('dblclick', this);
+    this.update();
   }
 
   /**
@@ -454,6 +455,9 @@ class ActiveNotebook extends NotebookRenderer {
       if (widget) {
         widget.focus();
       }
+      if (widget instanceof MarkdownCellWidget) {
+        (widget as MarkdownCellWidget).rendered = false;
+      }
     } else {
       this.addClass(COMMAND_CLASS);
       this.removeClass(EDIT_CLASS);
@@ -521,7 +525,7 @@ class ActiveNotebook extends NotebookRenderer {
   }
 
   private _mode: NotebookMode = 'command';
-  private _activeCellIndex = -1;
+  private _activeCellIndex = 0;
 }
 
 
@@ -563,11 +567,7 @@ namespace Private {
         // Do nothing.
       } else if ((mode as CodeMirror.modespec).name) {
         let name = (mode as CodeMirror.modespec).name;
-        if (CodeMirror.modes.hasOwnProperty(name)) {
-          mode = CodeMirror.modes[name];
-        } else {
-          mode = CodeMirror.findModeByName(mode as string);
-        }
+        mode = CodeMirror.findModeByName(name);
       }
       if (mode) {
         mime = (mode as CodeMirror.modespec).mime;

+ 7 - 0
src/notebook/notebook/widgetfactory.ts

@@ -22,6 +22,10 @@ import {
   Widget
 } from 'phosphor-widget';
 
+import {
+  findKernel
+} from '../kernel-selector';
+
 import {
   ToolbarItems
 } from './default-toolbar';
@@ -70,6 +74,9 @@ class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel> {
     let rendermime = this._rendermime.clone();
     if (kernel) {
       context.changeKernel(kernel);
+    } else {
+      let name = findKernel(model.defaultKernelName, model.defaultKernelLanguage, context.kernelSpecs);
+      context.changeKernel({ name });
     }
     let panel = new NotebookPanel(model, rendermime, context, this._clipboard);
     ToolbarItems.populateDefaults(panel);

+ 8 - 3
src/notebook/theme.css

@@ -12,8 +12,13 @@
 }
 
 
+.p-Widget.jp-Notebook-panel {
+  height: 100%;
+}
+
+
 .p-Widget.jp-Notebook-container {
-  overflow-y: auto;
+  overflow: auto;
   padding-left: 10px;
   padding-right: 10px;
   padding-bottom: 20px;
@@ -37,7 +42,7 @@
 }
 
 
-.jp-InputArea-editor.jp-CodeMirror {
+.jp-CodeMirror {
   border: 1px solid #cfcfcf;
   border-radius: 2px;
   background: #f7f7f7;
@@ -113,7 +118,7 @@
 }
 
 
-.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-otherSelected.jp-mod-active {
+.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-multiSelected.jp-mod-active {
   background: linear-gradient(to right, #42A5F5 -40px, #42A5F5 7px, #E3F2FD 7px, #E3F2FD 100%);
 }