Pārlūkot izejas kodu

Merge pull request #6335 from ian-r-rose/default-cell-type

Default cell type
Steven Silvester 6 gadi atpakaļ
vecāks
revīzija
7a6356480e

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

@@ -284,6 +284,13 @@
         "codeFolding": false
       }
     },
+    "defaultCell": {
+      "title": "Default cell type",
+      "description": "The default type (markdown, code, or raw) for new cells",
+      "type": "string",
+      "enum": ["code", "markdown", "raw"],
+      "default": "code"
+    },
     "kernelShutdown": {
       "title": "Shut down kernel",
       "description": "Whether to shut down or not the kernel when closing a notebook.",

+ 3 - 1
packages/notebook-extension/src/index.ts

@@ -23,6 +23,7 @@ import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
 import {
   ISettingRegistry,
   IStateDB,
+  nbformat,
   PageConfig,
   URLExt
 } from '@jupyterlab/coreutils';
@@ -626,7 +627,8 @@ function activateNotebookHandler(
     });
     factory.editorConfig = { code, markdown, raw };
     factory.notebookConfig = {
-      scrollPastEnd: settings.get('scrollPastEnd').composite as boolean
+      scrollPastEnd: settings.get('scrollPastEnd').composite as boolean,
+      defaultCell: settings.get('defaultCell').composite as nbformat.CellType
     };
     factory.shutdownOnClose = settings.get('kernelShutdown')
       .composite as boolean;

+ 22 - 5
packages/notebook/src/actions.tsx

@@ -262,7 +262,10 @@ export namespace NotebookActions {
 
     const state = Private.getState(notebook);
     const model = notebook.model;
-    const cell = model.contentFactory.createCodeCell({});
+    const cell = model.contentFactory.createCell(
+      notebook.notebookConfig.defaultCell,
+      {}
+    );
     const active = notebook.activeCellIndex;
 
     model.cells.insert(active, cell);
@@ -291,7 +294,10 @@ export namespace NotebookActions {
 
     const state = Private.getState(notebook);
     const model = notebook.model;
-    const cell = model.contentFactory.createCodeCell({});
+    const cell = model.contentFactory.createCell(
+      notebook.notebookConfig.defaultCell,
+      {}
+    );
 
     model.cells.insert(notebook.activeCellIndex + 1, cell);
 
@@ -446,7 +452,10 @@ export namespace NotebookActions {
     const model = notebook.model;
 
     if (notebook.activeCellIndex === notebook.widgets.length - 1) {
-      const cell = model.contentFactory.createCodeCell({});
+      const cell = model.contentFactory.createCell(
+        notebook.notebookConfig.defaultCell,
+        {}
+      );
 
       model.cells.push(cell);
       notebook.activeCellIndex++;
@@ -484,7 +493,10 @@ export namespace NotebookActions {
     const state = Private.getState(notebook);
     const promise = Private.runSelected(notebook, session);
     const model = notebook.model;
-    const cell = model.contentFactory.createCodeCell({});
+    const cell = model.contentFactory.createCell(
+      notebook.notebookConfig.defaultCell,
+      {}
+    );
 
     model.cells.insert(notebook.activeCellIndex + 1, cell);
     notebook.activeCellIndex++;
@@ -1658,7 +1670,12 @@ namespace Private {
       // within the compound operation to make the deletion of
       // a notebook's last cell undoable.
       if (!cells.length) {
-        cells.push(model.contentFactory.createCodeCell({}));
+        cells.push(
+          model.contentFactory.createCell(
+            notebook.notebookConfig.defaultCell,
+            {}
+          )
+        );
       }
       cells.endCompoundOperation();
 

+ 42 - 16
packages/notebook/src/model.ts

@@ -56,6 +56,7 @@ export interface INotebookModel extends DocumentRegistry.IModel {
    * The metadata associated with the notebook.
    */
   readonly metadata: IObservableJSON;
+
   /**
    * The array of deleted cells since the notebook was last run.
    */
@@ -74,10 +75,6 @@ export class NotebookModel extends DocumentModel implements INotebookModel {
     let factory = options.contentFactory || NotebookModel.defaultContentFactory;
     this.contentFactory = factory.clone(this.modelDB.view('cells'));
     this._cells = new CellList(this.modelDB, this.contentFactory);
-    // Add an initial code cell by default.
-    if (!this._cells.length) {
-      this._cells.push(factory.createCodeCell({}));
-    }
     this._cells.changed.connect(this._onCellsChanged, this);
 
     // Handle initial metadata.
@@ -131,12 +128,14 @@ export class NotebookModel extends DocumentModel implements INotebookModel {
     let spec = this.metadata.get('kernelspec') as nbformat.IKernelspecMetadata;
     return spec ? spec.name : '';
   }
+
   /**
-   * The default kernel name of the document.
+   * A list of deleted cells for the notebook..
    */
   get deletedCells(): string[] {
     return this._deletedCells;
   }
+
   /**
    * The default kernel language of the document.
    */
@@ -312,17 +311,6 @@ export class NotebookModel extends DocumentModel implements INotebookModel {
       default:
         break;
     }
-    let factory = this.contentFactory;
-    // Add code cell if there are no cells remaining.
-    if (!this.cells.length) {
-      // Add the cell in a new context to avoid triggering another
-      // cell changed event during the handling of this signal.
-      requestAnimationFrame(() => {
-        if (!this.isDisposed && !this.cells.length) {
-          this.cells.push(factory.createCodeCell({}));
-        }
-      });
-    }
     this.triggerContentChange();
   }
 
@@ -385,6 +373,19 @@ export namespace NotebookModel {
      */
     modelDB: IModelDB;
 
+    /**
+     * Create a new cell by cell type.
+     *
+     * @param type:  the type of the cell to create.
+     *
+     * @param options: the cell creation options.
+     *
+     * #### Notes
+     * This method is intended to be a convenience method to programmaticaly
+     * call the other cell creation methods in the factory.
+     */
+    createCell(type: nbformat.CellType, opts: CellModel.IOptions): ICellModel;
+
     /**
      * Create a new code cell.
      *
@@ -444,6 +445,31 @@ export namespace NotebookModel {
      */
     readonly modelDB: IModelDB | undefined;
 
+    /**
+     * Create a new cell by cell type.
+     *
+     * @param type:  the type of the cell to create.
+     *
+     * @param options: the cell creation options.
+     *
+     * #### Notes
+     * This method is intended to be a convenience method to programmaticaly
+     * call the other cell creation methods in the factory.
+     */
+    createCell(type: nbformat.CellType, opts: CellModel.IOptions): ICellModel {
+      switch (type) {
+        case 'code':
+          return this.createCodeCell(opts);
+          break;
+        case 'markdown':
+          return this.createMarkdownCell(opts);
+          break;
+        case 'raw':
+        default:
+          return this.createRawCell(opts);
+      }
+    }
+
     /**
      * Create a new code cell.
      *

+ 31 - 1
packages/notebook/src/widget.ts

@@ -382,6 +382,11 @@ export class StaticNotebook extends Widget {
     }
     this._updateMimetype();
     let cells = newValue.cells;
+    if (!cells.length) {
+      cells.push(
+        newValue.contentFactory.createCell(this.notebookConfig.defaultCell, {})
+      );
+    }
     each(cells, (cell: ICellModel, i: number) => {
       this._insertCell(i, cell);
     });
@@ -412,6 +417,22 @@ export class StaticNotebook extends Widget {
         each(args.oldValues, value => {
           this._removeCell(args.oldIndex);
         });
+        // Add default cell if there are no cells remaining.
+        if (!sender.length) {
+          const model = this.model;
+          // Add the cell in a new context to avoid triggering another
+          // cell changed event during the handling of this signal.
+          requestAnimationFrame(() => {
+            if (!model.isDisposed && !model.cells.length) {
+              model.cells.push(
+                model.contentFactory.createCell(
+                  this.notebookConfig.defaultCell,
+                  {}
+                )
+              );
+            }
+          });
+        }
         break;
       case 'set':
         // TODO: reuse existing widgets if possible.
@@ -442,6 +463,9 @@ export class StaticNotebook extends Widget {
         break;
       case 'markdown':
         widget = this._createMarkdownCell(cell as IMarkdownCellModel);
+        if (cell.value.text === '') {
+          (widget as MarkdownCell).rendered = false;
+        }
         break;
       default:
         widget = this._createRawCell(cell as IRawCellModel);
@@ -730,12 +754,18 @@ export namespace StaticNotebook {
      * Enable scrolling past the last cell
      */
     scrollPastEnd: boolean;
+
+    /**
+     * The default type for new notebook cells.
+     */
+    defaultCell: nbformat.CellType;
   }
   /**
    * Default configuration options for notebooks.
    */
   export const defaultNotebookConfig: INotebookConfig = {
-    scrollPastEnd: true
+    scrollPastEnd: true,
+    defaultCell: 'code'
   };
 
   /**

+ 21 - 31
tests/test-notebook/src/model.spec.ts

@@ -13,11 +13,7 @@ import { NotebookModel } from '@jupyterlab/notebook';
 
 import { ModelDB } from '@jupyterlab/observables';
 
-import {
-  signalToPromise,
-  NBTestUtils,
-  acceptDialog
-} from '@jupyterlab/testutils';
+import { acceptDialog, NBTestUtils } from '@jupyterlab/testutils';
 
 describe('@jupyterlab/notebook', () => {
   describe('NotebookModel', () => {
@@ -35,12 +31,6 @@ describe('@jupyterlab/notebook', () => {
         expect(lang.name).to.equal('python');
       });
 
-      it('should add a single code cell by default', () => {
-        const model = new NotebookModel();
-        expect(model.cells.length).to.equal(1);
-        expect(model.cells.get(0)).to.be.an.instanceof(CodeCellModel);
-      });
-
       it('should accept an optional factory', () => {
         const contentFactory = new NotebookModel.ContentFactory({});
         const model = new NotebookModel({ contentFactory });
@@ -78,12 +68,6 @@ describe('@jupyterlab/notebook', () => {
     });
 
     describe('#cells', () => {
-      it('should add an empty code cell by default', () => {
-        const model = new NotebookModel();
-        expect(model.cells.length).to.equal(1);
-        expect(model.cells.get(0)).to.be.an.instanceof(CodeCellModel);
-      });
-
       it('should be reset when loading from disk', () => {
         const model = new NotebookModel();
         const cell = model.contentFactory.createCodeCell({});
@@ -100,9 +84,9 @@ describe('@jupyterlab/notebook', () => {
         model.cells.push(cell);
         model.fromJSON(NBTestUtils.DEFAULT_CONTENT);
         model.cells.undo();
-        expect(model.cells.length).to.equal(2);
-        expect(model.cells.get(1).value.text).to.equal('foo');
-        expect(model.cells.get(1)).to.equal(cell); // should be ===.
+        expect(model.cells.length).to.equal(1);
+        expect(model.cells.get(0).value.text).to.equal('foo');
+        expect(model.cells.get(0)).to.equal(cell); // should be ===.
       });
 
       context('cells `changed` signal', () => {
@@ -149,17 +133,6 @@ describe('@jupyterlab/notebook', () => {
           model.cells.push(cell);
           expect(model.dirty).to.equal(true);
         });
-
-        it('should add a new code cell when cells are cleared', async () => {
-          const model = new NotebookModel();
-          let promise = signalToPromise(model.cells.changed);
-          model.cells.clear();
-          await promise;
-          expect(model.cells.length).to.equal(0);
-          await signalToPromise(model.cells.changed);
-          expect(model.cells.length).to.equal(1);
-          expect(model.cells.get(0)).to.be.an.instanceof(CodeCellModel);
-        });
       });
 
       describe('cell `changed` signal', () => {
@@ -393,6 +366,23 @@ describe('@jupyterlab/notebook', () => {
         });
       });
 
+      context('#createCell()', () => {
+        it('should create a new code cell', () => {
+          const cell = factory.createCell('code', {});
+          expect(cell.type).to.equal('code');
+        });
+
+        it('should create a new code cell', () => {
+          const cell = factory.createCell('markdown', {});
+          expect(cell.type).to.equal('markdown');
+        });
+
+        it('should create a new code cell', () => {
+          const cell = factory.createCell('raw', {});
+          expect(cell.type).to.equal('raw');
+        });
+      });
+
       context('#createCodeCell()', () => {
         it('should create a new code cell', () => {
           const cell = factory.createCodeCell({});

+ 0 - 7
tests/test-notebook/src/modelfactory.spec.ts

@@ -93,13 +93,6 @@ describe('@jupyterlab/notebook', () => {
         expect(model).to.be.an.instanceof(NotebookModel);
       });
 
-      it('should add an empty code cell by default', () => {
-        const factory = new NotebookModelFactory({});
-        const model = factory.createNew();
-        expect(model.cells.length).to.equal(1);
-        expect(model.cells.get(0).type).to.equal('code');
-      });
-
       it('should accept a language preference', () => {
         const factory = new NotebookModelFactory({});
         const model = factory.createNew('foo');

+ 56 - 1
tests/test-notebook/src/widget.spec.ts

@@ -23,7 +23,11 @@ import { INotebookModel, NotebookModel } from '@jupyterlab/notebook';
 
 import { Notebook, StaticNotebook } from '@jupyterlab/notebook';
 
-import { NBTestUtils, framePromise } from '@jupyterlab/testutils';
+import {
+  NBTestUtils,
+  framePromise,
+  signalToPromise
+} from '@jupyterlab/testutils';
 
 const contentFactory = NBTestUtils.createNotebookFactory();
 const editorConfig = NBTestUtils.defaultEditorConfig;
@@ -253,6 +257,25 @@ describe('@jupyter/notebook', () => {
         expect(widget.widgets.length).to.equal(6);
       });
 
+      it('should add a default cell if the notebook model is empty', () => {
+        const widget = new LogStaticNotebook(options);
+        const model1 = new NotebookModel();
+        expect(model1.cells.length).to.equal(0);
+        widget.model = model1;
+        expect(model1.cells.length).to.equal(1);
+        expect(model1.cells.get(0).type).to.equal('code');
+
+        widget.notebookConfig = {
+          ...widget.notebookConfig,
+          defaultCell: 'markdown'
+        };
+        const model2 = new NotebookModel();
+        expect(model2.cells.length).to.equal(0);
+        widget.model = model2;
+        expect(model2.cells.length).to.equal(1);
+        expect(model2.cells.get(0).type).to.equal('markdown');
+      });
+
       it('should set the mime types of the cell widgets', () => {
         const widget = new LogStaticNotebook(options);
         const model = new NotebookModel();
@@ -298,6 +321,19 @@ describe('@jupyter/notebook', () => {
           expect(child.hasClass('jp-Notebook-cell')).to.equal(true);
         });
 
+        it('should initially render markdown cells with content', () => {
+          const cell1 = widget.model.contentFactory.createMarkdownCell({});
+          const cell2 = widget.model.contentFactory.createMarkdownCell({});
+          cell1.value.text = '# Hello';
+          widget.model.cells.push(cell1);
+          widget.model.cells.push(cell2);
+          expect(widget.widgets.length).to.equal(8);
+          const child1 = widget.widgets[6] as MarkdownCell;
+          const child2 = widget.widgets[7] as MarkdownCell;
+          expect(child1.rendered).to.equal(true);
+          expect(child2.rendered).to.equal(false);
+        });
+
         it('should handle a move', () => {
           const child = widget.widgets[1];
           widget.model.cells.move(1, 2);
@@ -310,6 +346,21 @@ describe('@jupyter/notebook', () => {
           widget.model.cells.clear();
           expect(widget.widgets.length).to.equal(0);
         });
+
+        it('should add a new default cell when cells are cleared', async () => {
+          const model = widget.model;
+          widget.notebookConfig = {
+            ...widget.notebookConfig,
+            defaultCell: 'raw'
+          };
+          let promise = signalToPromise(model.cells.changed);
+          model.cells.clear();
+          await promise;
+          expect(model.cells.length).to.equal(0);
+          await signalToPromise(model.cells.changed);
+          expect(model.cells.length).to.equal(1);
+          expect(model.cells.get(0)).to.be.an.instanceof(RawCellModel);
+        });
       });
     });
 
@@ -648,6 +699,7 @@ describe('@jupyter/notebook', () => {
         Widget.attach(widget, document.body);
         MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
         const cell = widget.model.contentFactory.createMarkdownCell({});
+        cell.value.text = '# Hello'; // Should be rendered with content.
         widget.model.cells.push(cell);
         const child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
         expect(child.rendered).to.equal(true);
@@ -1105,6 +1157,7 @@ describe('@jupyter/notebook', () => {
 
         it('should preserve "command" mode if in a markdown cell', () => {
           const cell = widget.model.contentFactory.createMarkdownCell({});
+          cell.value.text = '# Hello'; // Should be rendered with content.
           widget.model.cells.push(cell);
           const count = widget.widgets.length;
           const child = widget.widgets[count - 1] as MarkdownCell;
@@ -1156,6 +1209,7 @@ describe('@jupyter/notebook', () => {
         it('should leave a markdown cell rendered', () => {
           const code = widget.model.contentFactory.createCodeCell({});
           const md = widget.model.contentFactory.createMarkdownCell({});
+          md.value.text = '# Hello'; // Should be rendered with content.
           widget.model.cells.push(code);
           widget.model.cells.push(md);
           const count = widget.widgets.length;
@@ -1214,6 +1268,7 @@ describe('@jupyter/notebook', () => {
       context('dblclick', () => {
         it('should unrender a markdown cell', () => {
           const cell = widget.model.contentFactory.createMarkdownCell({});
+          cell.value.text = '# Hello'; // Should be rendered with content.
           widget.model.cells.push(cell);
           const child = widget.widgets[
             widget.widgets.length - 1