Sfoglia il codice sorgente

Merge branch 'master' into print-notebook

Saul Shanabrook 6 anni fa
parent
commit
d225704414

+ 4 - 0
.gitignore

@@ -2,16 +2,20 @@ MANIFEST
 build
 dist
 lib
+
 jupyterlab/static
 jupyterlab/schemas
 jupyterlab/themes
 jupyterlab/geckodriver
+
 dev_mode/schemas
 dev_mode/static
 dev_mode/themes
 dev_mode/workspaces
 dev_mode/stats.json
 
+packages/nbconvert-css/style/
+
 packages/theme-*/static
 node_modules
 .cache

+ 2 - 2
CONTRIBUTING.md

@@ -212,8 +212,8 @@ jlpm create:test <package-directory-name>
 #### Running Jest Tests
 
 For those test folders that use `jest`, they can be run as `jlpm test` to run the files
-directly. You can also use `jlpm test --namePattern=<regex>` to specify specific test
-suite names, and `jlpm test --pathPattern=<regex>` to specify specific test module names. In order to watch the code, add a `debugger` line in your code and run `jlpm watch`. This will start a node V8 debugger, which can be debugged
+directly. You can also use `jlpm test --testNamePattern=<regex>` to specify specific test
+suite names, and `jlpm test --testPathPattern=<regex>` to specify specific test module names. In order to watch the code, add a `debugger` line in your code and run `jlpm watch`. This will start a node V8 debugger, which can be debugged
 in Chrome by browsing to `chrome://inspect/` and launching the remote session.
 
 ### Build and run the stand-alone examples

+ 1 - 0
README.md

@@ -8,6 +8,7 @@
 # [JupyterLab](http://jupyterlab.github.io/jupyterlab/)
 
 [![PyPI version](https://badge.fury.io/py/jupyterlab.svg)](https://badge.fury.io/py/jupyterlab)
+[![Downloads](https://pepy.tech/badge/jupyterlab/month)](https://pepy.tech/project/jupyterlab/month)
 [![Build Status](https://dev.azure.com/jupyterlab/jupyterlab/_apis/build/status/jupyterlab.jupyterlab?branchName=master)](https://dev.azure.com/jupyterlab/jupyterlab/_build/latest?definitionId=1&branchName=master)
 [![Documentation Status](https://readthedocs.org/projects/jupyterlab/badge/?version=stable)](http://jupyterlab.readthedocs.io/en/stable/)
 [![GitHub](https://img.shields.io/badge/issue_tracking-github-blue.svg)](https://github.com/jupyterlab/jupyterlab/issues)

+ 3 - 1
dev_mode/package.json

@@ -57,6 +57,7 @@
     "@jupyterlab/markdownviewer-extension": "^1.0.0-alpha.6",
     "@jupyterlab/mathjax2": "^1.0.0-alpha.6",
     "@jupyterlab/mathjax2-extension": "^1.0.0-alpha.6",
+    "@jupyterlab/nbconvert-css": "^0.1.0",
     "@jupyterlab/notebook": "^1.0.0-alpha.7",
     "@jupyterlab/notebook-extension": "^1.0.0-alpha.6",
     "@jupyterlab/observables": "^2.2.0-alpha.6",
@@ -98,7 +99,7 @@
     "@phosphor/virtualdom": "^1.1.2",
     "@phosphor/widgets": "^1.6.0",
     "ajv": "^6.5.5",
-    "codemirror": "~5.42.0",
+    "codemirror": "~5.46.0",
     "comment-json": "^1.1.3",
     "es6-promise": "~4.1.1",
     "marked": "0.5.1",
@@ -282,6 +283,7 @@
       "@jupyterlab/markdownviewer-extension": "../packages/markdownviewer-extension",
       "@jupyterlab/mathjax2": "../packages/mathjax2",
       "@jupyterlab/mathjax2-extension": "../packages/mathjax2-extension",
+      "@jupyterlab/nbconvert-css": "../packages/nbconvert-css",
       "@jupyterlab/notebook": "../packages/notebook",
       "@jupyterlab/notebook-extension": "../packages/notebook-extension",
       "@jupyterlab/observables": "../packages/observables",

+ 29 - 28
jupyterlab/commands.py

@@ -90,6 +90,24 @@ def get_app_dir():
     return osp.abspath(app_dir)
 
 
+def dedupe_yarn(path, logger=None):
+    """ `yarn-deduplicate` with the `fewer` strategy to minimize total
+        packages installed in a given staging directory
+
+        This means a extension (or dependency) _could_ cause a downgrade of an
+        version expected at publication time, but core should aggressively set
+        pins above, for example, known-bad versions
+    """
+    had_dupes = Process(
+        ['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
+        cwd=path, logger=logger
+    ).wait() != 0
+
+    if had_dupes:
+        yarn_proc = Process(['node', YARN_PATH], cwd=path, logger=logger)
+        yarn_proc.wait()
+
+
 def ensure_dev(logger=None):
     """Ensure that the dev assets are available.
     """
@@ -99,12 +117,7 @@ def ensure_dev(logger=None):
         yarn_proc = Process(['node', YARN_PATH], cwd=parent, logger=logger)
         yarn_proc.wait()
 
-        yarn_proc = Process(['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
-                            cwd=parent, logger=logger)
-        had_dupes = yarn_proc.wait() != 0
-        if had_dupes:
-            yarn_proc = Process(['node', YARN_PATH], cwd=parent, logger=logger)
-            yarn_proc.wait()
+        dedupe_yarn(parent, logger)
 
     if not osp.exists(pjoin(parent, 'dev_mode', 'static')):
         yarn_proc = Process(['node', YARN_PATH, 'build'], cwd=parent,
@@ -125,12 +138,7 @@ def ensure_core(logger=None):
         yarn_proc = Process(['node', YARN_PATH], cwd=staging, logger=logger)
         yarn_proc.wait()
 
-        yarn_proc = Process(['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
-                            cwd=staging, logger=logger)
-        had_dupes = yarn_proc.wait() != 0
-        if had_dupes:
-            yarn_proc = Process(['node', YARN_PATH], cwd=staging, logger=logger)
-            yarn_proc.wait()
+        dedupe_yarn(staging, logger)
 
     if not osp.exists(pjoin(HERE, 'static')):
         yarn_proc = Process(['node', YARN_PATH, 'build'], cwd=staging,
@@ -156,12 +164,7 @@ def watch_packages(logger=None):
         yarn_proc = Process(['node', YARN_PATH], cwd=parent, logger=logger)
         yarn_proc.wait()
 
-        yarn_proc = Process(['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
-                            cwd=parent, logger=logger)
-        had_dupes = yarn_proc.wait() != 0
-        if had_dupes:
-            yarn_proc = Process(['node', YARN_PATH], cwd=parent, logger=logger)
-            yarn_proc.wait()
+        dedupe_yarn(parent, logger)
 
     logger = _ensure_logger(logger)
     ts_dir = osp.abspath(osp.join(HERE, '..', 'packages', 'metapackage'))
@@ -468,11 +471,8 @@ class _AppHandler(object):
             msg = 'npm dependencies failed to install'
             self.logger.error(msg)
             raise RuntimeError(msg)
-        had_dupes = 0 != self._run(
-            ['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
-            cwd=staging)
-        if had_dupes:
-            self._run(['node', YARN_PATH, 'install'], cwd=staging)
+
+        dedupe_yarn(staging, self.logger)
 
         # Build the app.
         ret = self._run(['node', YARN_PATH, 'run', command], cwd=staging)
@@ -491,11 +491,7 @@ class _AppHandler(object):
 
         # Make sure packages are installed.
         self._run(['node', YARN_PATH, 'install'], cwd=staging)
-        had_dupes = 0 != self._run(
-            ['node', YARN_PATH, 'yarn-deduplicate', '-s', 'fewer', '--fail'],
-            cwd=staging)
-        if had_dupes:
-            self._run(['node', YARN_PATH, 'install'], cwd=staging)
+        dedupe_yarn(staging, self.logger)
 
         proc = WatchHelper(['node', YARN_PATH, 'run', 'watch'],
                            cwd=pjoin(self.app_dir, 'staging'),
@@ -979,6 +975,11 @@ class _AppHandler(object):
         with open(pkg_path, 'w') as fid:
             json.dump(data, fid, indent=4)
 
+        # copy known-good yarn.lock if missing
+        lock_path = pjoin(staging, 'yarn.lock')
+        if not osp.exists(lock_path):
+            shutil.copy(pjoin(HERE, 'staging', 'yarn.lock'), lock_path)
+
     def _get_package_template(self, silent=False):
         """Get the template the for staging package.json file.
         """

+ 18 - 46
jupyterlab/staging/yarn.lock

@@ -1995,7 +1995,7 @@ color-name@1.1.3:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-commander@2, commander@^2.19.0, commander@^2.5.0, commander@~2.20.0:
+commander@2, commander@^2.10.0, commander@^2.19.0, commander@^2.5.0:
   version "2.20.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
   integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -2277,12 +2277,12 @@ cyclist@~0.2.2:
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
   integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
 
-d3-array@1, d3-array@^1.1.1:
+d3-array@1, d3-array@^1.1.1, "d3-array@^1.2.0 || 2":
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
   integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
 
-"d3-array@^1.2.0 || 2", d3-array@^2.0.3:
+d3-array@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.0.3.tgz#9c0531eda701e416f28a030e3d4e6179ba74f19f"
   integrity sha512-C7g4aCOoJa+/K5hPVqZLG8wjYHsTUROTk7Z1Ep9F4P5l+WVrvV0+6nAZ1wKTRLMhFWpGbozxUpyjIPZYAaLi+g==
@@ -3855,16 +3855,11 @@ lower-case@^1.1.1:
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
   integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
 
-lowercase-keys@1.0.0:
+lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
   integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=
 
-lowercase-keys@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
-  integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
-
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -4014,7 +4009,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@0.0.8:
+minimist@0.0.8, minimist@~0.0.1:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
   integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
@@ -4024,11 +4019,6 @@ minimist@^1.2.0, minimist@~1.2.0:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-  integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
-
 minipass@^2.2.1, minipass@^2.3.4:
   version "2.3.5"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
@@ -4739,16 +4729,11 @@ pumpify@^1.3.3:
     inherits "^2.0.3"
     pump "^2.0.0"
 
-punycode@1.3.2:
+punycode@1.3.2, punycode@^1.2.4:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
   integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
 
-punycode@^1.2.4:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
-
 punycode@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -5477,14 +5462,7 @@ string-width@^3.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string_decoder@^1.0.0, string_decoder@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
-  integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
-  dependencies:
-    safe-buffer "~5.1.0"
-
-string_decoder@~1.1.1:
+string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
   integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
@@ -5733,7 +5711,7 @@ ua-parser-js@^0.7.18, ua-parser-js@^0.7.9:
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
   integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
 
-uglify-js@3.4.x:
+uglify-js@3.4.x, uglify-js@^3.1.4:
   version "3.4.10"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
   integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
@@ -5741,14 +5719,6 @@ uglify-js@3.4.x:
     commander "~2.19.0"
     source-map "~0.6.1"
 
-uglify-js@^3.1.4:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.4.tgz#4a64d57f590e20a898ba057f838dcdfb67a939b9"
-  integrity sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA==
-  dependencies:
-    commander "~2.20.0"
-    source-map "~0.6.1"
-
 union-value@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@@ -5873,20 +5843,13 @@ util.promisify@1.0.0, util.promisify@~1.0.0:
     define-properties "^1.1.2"
     object.getownpropertydescriptors "^2.0.3"
 
-util@0.10.3:
+util@0.10.3, util@^0.10.3:
   version "0.10.3"
   resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
   integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
   dependencies:
     inherits "2.0.1"
 
-util@^0.10.3:
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
-  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
-  dependencies:
-    inherits "2.0.3"
-
 util@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
@@ -6440,3 +6403,12 @@ yargs@~13.2.2:
     which-module "^2.0.0"
     y18n "^4.0.0"
     yargs-parser "^13.0.0"
+
+yarn-deduplicate@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.1.tgz#19b4a87654b66f55bf3a4bd6b153b4e4ab1b6e6d"
+  integrity sha512-2FDJ1dFmtvqhRmfja89ohYzpaheCYg7BFBSyaUq+kxK0y61C9oHv1XaQovCWGJtP2WU8PksQOgzMVV7oQOobzw==
+  dependencies:
+    "@yarnpkg/lockfile" "^1.1.0"
+    commander "^2.10.0"
+    semver "^5.3.0"

+ 1 - 0
packages/apputils/src/index.ts

@@ -12,6 +12,7 @@ export * from './dialog';
 export * from './domutils';
 export * from './hoverbox';
 export * from './iframe';
+export * from './inputdialog';
 export * from './instancetracker';
 export * from './mainareawidget';
 export * from './printing';

+ 274 - 0
packages/apputils/src/inputdialog.ts

@@ -0,0 +1,274 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { Widget } from '@phosphor/widgets';
+
+import { Dialog } from './dialog';
+import { Styling } from './styling';
+
+const INPUT_DIALOG_CLASS = 'jp-Input-Dialog';
+
+/**
+ * Create and show a input dialog for a number.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getNumber(
+  options: InputDialog.INumberOptions
+): Promise<Dialog.IResult<number>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputNumberDialog(options)
+  });
+  return dialog.launch();
+}
+
+/**
+ * Create and show a input dialog for a choice.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getItem(
+  options: InputDialog.IItemOptions
+): Promise<Dialog.IResult<string>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputItemsDialog(options)
+  });
+  return dialog.launch();
+}
+
+/**
+ * Create and show a input dialog for a text.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getText(
+  options: InputDialog.ITextOptions
+): Promise<Dialog.IResult<string>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputTextDialog(options)
+  });
+  return dialog.launch();
+}
+
+export namespace InputDialog {
+  export interface IOptions<T>
+    extends Partial<
+      Pick<
+        Dialog.IOptions<T>,
+        Exclude<keyof Dialog.IOptions<T>, 'body' | 'buttons' | 'defaultButton'>
+      >
+    > {
+    /**
+     * Label of the requested input
+     */
+    label: string;
+  }
+
+  export interface INumberOptions extends IOptions<Number> {
+    /**
+     * Default value
+     */
+    value?: number;
+  }
+
+  export interface IItemOptions extends IOptions<string> {
+    /**
+     * List of choices
+     */
+    items: Array<string>;
+    /**
+     * Default choice
+     *
+     * If the list is editable a string with a default value can be provided
+     * otherwise the index of the default choice should be given.
+     */
+    current?: number | string;
+    /**
+     * Is the item editable?
+     */
+    editable?: boolean;
+    /**
+     * Placeholder text for editable input
+     */
+    placeholder?: string;
+  }
+
+  export interface ITextOptions extends IOptions<string> {
+    /**
+     * Default input text
+     */
+    text?: string;
+    /**
+     * Placeholder text
+     */
+    placeholder?: string;
+  }
+}
+
+/**
+ * Base widget for input dialog body
+ */
+class InputDialog<T> extends Widget implements Dialog.IBodyWidget<T> {
+  /**
+   * InputDialog constructor
+   *
+   * @param label Input field label
+   */
+  constructor(label: string) {
+    super();
+    this.addClass(INPUT_DIALOG_CLASS);
+
+    let labelElement = document.createElement('label');
+    labelElement.textContent = label;
+
+    // Initialize the node
+    this.node.appendChild(labelElement);
+  }
+
+  /** Input HTML node */
+  protected _input: HTMLInputElement;
+}
+
+/**
+ * Widget body for input number dialog
+ */
+class InputNumberDialog extends InputDialog<number> {
+  /**
+   * InputNumberDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.INumberOptions) {
+    super(options.label);
+
+    this._input = document.createElement('input', {});
+    this._input.classList.add('jp-mod-styled');
+    this._input.type = 'number';
+    this._input.value = options.value ? options.value.toString() : '0';
+
+    // Initialize the node
+    this.node.appendChild(this._input);
+  }
+
+  /**
+   * Get the number specified by the user.
+   */
+  getValue(): number {
+    if (this._input.value) {
+      return Number(this._input.value);
+    } else {
+      return Number.NaN;
+    }
+  }
+}
+
+/**
+ * Widget body for input text dialog
+ */
+class InputTextDialog extends InputDialog<string> {
+  /**
+   * InputTextDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.ITextOptions) {
+    super(options.label);
+
+    this._input = document.createElement('input', {});
+    this._input.classList.add('jp-mod-styled');
+    this._input.type = 'text';
+    this._input.value = options.text ? options.text : '';
+    if (options.placeholder) {
+      this._input.placeholder = options.placeholder;
+    }
+
+    // Initialize the node
+    this.node.appendChild(this._input);
+  }
+
+  /**
+   * Get the text specified by the user
+   */
+  getValue(): string {
+    return this._input.value;
+  }
+}
+
+/**
+ * Widget body for input list dialog
+ */
+class InputItemsDialog extends InputDialog<string> {
+  /**
+   * InputItemsDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.IItemOptions) {
+    super(options.label);
+
+    this._editable = options.editable || false;
+
+    let current = options.current || 0;
+    let defaultIndex: number;
+    if (typeof current === 'number') {
+      defaultIndex = Math.max(0, Math.min(current, options.items.length - 1));
+      current = '';
+    }
+
+    this._list = document.createElement('select');
+    options.items.forEach((item, index) => {
+      let option = document.createElement('option');
+      if (index === defaultIndex) {
+        option.selected = true;
+        current = item;
+      }
+      option.value = item;
+      option.textContent = item;
+      this._list.appendChild(option);
+    });
+
+    if (options.editable) {
+      /* Use of list and datalist */
+      let data = document.createElement('datalist');
+      data.id = 'input-dialog-items';
+      data.appendChild(this._list);
+
+      this._input = document.createElement('input', {});
+      this._input.classList.add('jp-mod-styled');
+      this._input.type = 'list';
+      this._input.value = current;
+      this._input.setAttribute('list', data.id);
+      if (options.placeholder) {
+        this._input.placeholder = options.placeholder;
+      }
+      this.node.appendChild(this._input);
+      this.node.appendChild(data);
+    } else {
+      /* Use select directly */
+      this.node.appendChild(Styling.wrapSelect(this._list));
+    }
+  }
+
+  /**
+   * Get the user choice
+   */
+  getValue(): string {
+    if (this._editable) {
+      return this._input.value;
+    } else {
+      return this._list.value;
+    }
+  }
+
+  private _list: HTMLSelectElement;
+  private _editable: boolean;
+}

+ 1 - 1
packages/codemirror-extension/package.json

@@ -40,7 +40,7 @@
     "@jupyterlab/mainmenu": "^1.0.0-alpha.6",
     "@jupyterlab/statusbar": "^1.0.0-alpha.6",
     "@phosphor/widgets": "^1.6.0",
-    "codemirror": "~5.42.0"
+    "codemirror": "~5.46.0"
   },
   "devDependencies": {
     "rimraf": "~2.6.2",

+ 1 - 1
packages/codemirror/package.json

@@ -42,7 +42,7 @@
     "@phosphor/disposable": "^1.1.2",
     "@phosphor/signaling": "^1.2.2",
     "@phosphor/widgets": "^1.6.0",
-    "codemirror": "~5.42.0",
+    "codemirror": "~5.46.0",
     "react": "~16.8.4"
   },
   "devDependencies": {

+ 5 - 0
packages/coreutils/src/poll.ts

@@ -309,6 +309,11 @@ export class Poll<T = any, U = any> implements IDisposable, IPoll<T, U> {
    * Refreshes the poll. Schedules `refreshed` tick if necessary.
    *
    * @returns A promise that resolves after tick is scheduled and never rejects.
+   *
+   * #### Notes
+   * The returned promise resolves after the tick is scheduled, but before
+   * the polling action is run. To wait until after the poll action executes,
+   * await the `poll.tick` promise: `await poll.refresh(); await poll.tick;`
    */
   refresh(): Promise<void> {
     return this.schedule({

+ 1 - 1
packages/docmanager-extension/src/index.ts

@@ -586,7 +586,7 @@ function addLabCommands(
       return labShell.currentWidget;
     }
     const pathMatch = node['title'].match(pathRe);
-    return docManager.findWidget(pathMatch[1]);
+    return docManager.findWidget(pathMatch[1], null);
   };
 
   // Returns `true` if the current widget has a document context.

+ 1 - 1
packages/documentsearch/package.json

@@ -38,7 +38,7 @@
     "@phosphor/disposable": "^1.1.2",
     "@phosphor/signaling": "^1.2.2",
     "@phosphor/widgets": "^1.6.0",
-    "codemirror": "~5.42.0",
+    "codemirror": "~5.46.0",
     "react": "~16.8.4"
   },
   "devDependencies": {

+ 19 - 55
packages/filebrowser/src/model.ts

@@ -5,7 +5,8 @@ import {
   IChangedArgs,
   IStateDB,
   PathExt,
-  PageConfig
+  PageConfig,
+  Poll
 } from '@jupyterlab/coreutils';
 
 import { IDocumentManager, shouldOverwrite } from '@jupyterlab/docmanager';
@@ -34,11 +35,6 @@ import { showDialog, Dialog } from '@jupyterlab/apputils';
  */
 const DEFAULT_REFRESH_INTERVAL = 10000;
 
-/**
- * The enforced time between refreshes in ms.
- */
-const MIN_REFRESH = 1000;
-
 /**
  * The maximum upload size (in bytes) for notebook version < 5.1.0
  */
@@ -87,8 +83,7 @@ export class FileBrowserModel implements IDisposable {
       format: 'text'
     };
     this._state = options.state || null;
-    this._baseRefreshDuration =
-      options.refreshInterval || DEFAULT_REFRESH_INTERVAL;
+    const refreshInterval = options.refreshInterval || DEFAULT_REFRESH_INTERVAL;
 
     const { services } = options.manager;
     services.contents.fileChanged.connect(this._onFileChanged, this);
@@ -103,8 +98,15 @@ export class FileBrowserModel implements IDisposable {
       }
     };
     window.addEventListener('beforeunload', this._unloadEventListener);
-    this._scheduleUpdate();
-    this._startTimer();
+    this._poll = new Poll({
+      factory: () => this.cd('.'),
+      frequency: {
+        interval: refreshInterval,
+        backoff: true,
+        max: 300 * 1000
+      },
+      standby: 'when-hidden'
+    });
   }
 
   /**
@@ -198,7 +200,7 @@ export class FileBrowserModel implements IDisposable {
     }
     window.removeEventListener('beforeunload', this._unloadEventListener);
     this._isDisposed = true;
-    clearTimeout(this._timeoutId);
+    this._poll.dispose();
     this._sessions.length = 0;
     this._items.length = 0;
     Signal.clearData(this);
@@ -225,10 +227,9 @@ export class FileBrowserModel implements IDisposable {
   /**
    * Force a refresh of the directory contents.
    */
-  refresh(): Promise<void> {
-    this._lastRefresh = new Date().getTime();
-    this._requested = false;
-    return this.cd('.');
+  async refresh(): Promise<void> {
+    await this._poll.refresh();
+    await this._poll.tick;
   }
 
   /**
@@ -269,9 +270,9 @@ export class FileBrowserModel implements IDisposable {
         if (this.isDisposed) {
           return;
         }
-        this._refreshDuration = this._baseRefreshDuration;
         this._handleContents(contents);
         this._pendingPath = null;
+        this._pending = null;
         if (oldValue !== newValue) {
           // If there is a state database and a unique key, save the new path.
           // We don't need to wait on the save to continue.
@@ -296,7 +297,6 @@ export class FileBrowserModel implements IDisposable {
           this._connectionFailure.emit(error);
           return this.cd('/');
         } else {
-          this._refreshDuration = this._baseRefreshDuration * 10;
           this._connectionFailure.emit(error);
         }
       });
@@ -586,7 +586,7 @@ export class FileBrowserModel implements IDisposable {
 
     // If either the old value or the new value is in the current path, update.
     if (value) {
-      this._scheduleUpdate();
+      void this._poll.refresh();
       this._populateSessions(sessions.running());
       this._fileChanged.emit(change);
       return;
@@ -605,38 +605,6 @@ export class FileBrowserModel implements IDisposable {
     });
   }
 
-  /**
-   * Start the internal refresh timer.
-   */
-  private _startTimer(): void {
-    this._timeoutId = window.setInterval(() => {
-      if (this._requested) {
-        void this.refresh();
-        return;
-      }
-      if (document.hidden) {
-        // Don't poll when nobody's looking.
-        return;
-      }
-      let date = new Date().getTime();
-      if (date - this._lastRefresh > this._refreshDuration) {
-        void this.refresh();
-      }
-    }, MIN_REFRESH);
-  }
-
-  /**
-   * Handle internal model refresh logic.
-   */
-  private _scheduleUpdate(): void {
-    let date = new Date().getTime();
-    if (date - this._lastRefresh > MIN_REFRESH) {
-      void this.refresh();
-    } else {
-      this._requested = true;
-    }
-  }
-
   private _connectionFailure = new Signal<this, Error>(this);
   private _fileChanged = new Signal<this, Contents.IChangedArgs>(this);
   private _items: Contents.IModel[] = [];
@@ -647,19 +615,15 @@ export class FileBrowserModel implements IDisposable {
   private _pending: Promise<void> | null = null;
   private _pendingPath: string | null = null;
   private _refreshed = new Signal<this, void>(this);
-  private _lastRefresh = -1;
-  private _requested = false;
   private _sessions: Session.IModel[] = [];
   private _state: IStateDB | null = null;
-  private _timeoutId = -1;
-  private _refreshDuration: number;
-  private _baseRefreshDuration: number;
   private _driveName: string;
   private _isDisposed = false;
   private _restored = new PromiseDelegate<void>();
   private _uploads: IUploadModel[] = [];
   private _uploadChanged = new Signal<this, IChangedArgs<IUploadModel>>(this);
   private _unloadEventListener: (e: Event) => string;
+  private _poll: Poll;
 }
 
 /**

+ 1 - 0
packages/metapackage/package.json

@@ -75,6 +75,7 @@
     "@jupyterlab/markdownviewer-extension": "^1.0.0-alpha.6",
     "@jupyterlab/mathjax2": "^1.0.0-alpha.6",
     "@jupyterlab/mathjax2-extension": "^1.0.0-alpha.6",
+    "@jupyterlab/nbconvert-css": "^0.1.0",
     "@jupyterlab/notebook": "^1.0.0-alpha.7",
     "@jupyterlab/notebook-extension": "^1.0.0-alpha.6",
     "@jupyterlab/observables": "^2.2.0-alpha.6",

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -141,6 +141,9 @@
     {
       "path": "../mathjax2-extension"
     },
+    {
+      "path": "../nbconvert-css"
+    },
     {
       "path": "../notebook"
     },

+ 60 - 0
packages/nbconvert-css/package.json

@@ -0,0 +1,60 @@
+{
+  "name": "@jupyterlab/nbconvert-css",
+  "version": "0.1.0",
+  "description": "CSS bundle for the JupyterLab nbconvert template",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "files": [
+    "lib/*.d.ts",
+    "lib/*.js.map",
+    "lib/*.js",
+    "style/*.css"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "directories": {
+    "lib": "lib/"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "scripts": {
+    "build": "tsc -b && webpack && rimraf style/index.js",
+    "clean": "rimraf lib/ & rimraf style/",
+    "prepare": "npm run build",
+    "watch": "tsc -b --watch"
+  },
+  "dependencies": {
+    "@jupyterlab/apputils": "^1.0.0-alpha.6",
+    "@jupyterlab/cells": "^1.0.0-alpha.7",
+    "@jupyterlab/codemirror": "^1.0.0-alpha.6",
+    "@jupyterlab/notebook": "^1.0.0-alpha.7",
+    "@jupyterlab/outputarea": "^1.0.0-alpha.6",
+    "@jupyterlab/rendermime": "^1.0.0-alpha.6"
+  },
+  "devDependencies": {
+    "@jupyterlab/apputils": "^1.0.0-alpha.6",
+    "@jupyterlab/cells": "^1.0.0-alpha.7",
+    "@jupyterlab/codemirror": "^1.0.0-alpha.6",
+    "@jupyterlab/notebook": "^1.0.0-alpha.7",
+    "@jupyterlab/outputarea": "^1.0.0-alpha.6",
+    "@jupyterlab/rendermime": "^1.0.0-alpha.6",
+    "css-loader": "~2.1.1",
+    "file-loader": "~3.0.1",
+    "mini-css-extract-plugin": "~0.6.0",
+    "null-loader": "^1.0.0",
+    "rimraf": "~2.6.2",
+    "typescript": "~3.4.3",
+    "url-loader": "~1.1.2",
+    "webpack": "~4.29.6",
+    "webpack-cli": "^3.3.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 21 - 0
packages/nbconvert-css/src/index.ts

@@ -0,0 +1,21 @@
+/* @jupyterlab/cells */
+import '@jupyterlab/cells/style/collapser.css';
+import '@jupyterlab/cells/style/headerfooter.css';
+import '@jupyterlab/cells/style/inputarea.css';
+import '@jupyterlab/cells/style/widget.css';
+
+/* @jupyterlab/outputarea */
+import '@jupyterlab/outputarea/style/index.css';
+
+/* @jupyterlab/notebook */
+import '@jupyterlab/notebook/style/index.css';
+
+/* @jupyterlab/rendermime */
+import '@jupyterlab/rendermime/style/index.css';
+
+/* @jupyterlab/apputils */
+import '@jupyterlab/apputils/style/materialcolors.css';
+import '@jupyterlab/apputils/style/styling.css';
+
+/* @jupyterlab/codemirror */
+import '@jupyterlab/codemirror/style/index.css';

+ 28 - 0
packages/nbconvert-css/tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../cells"
+    },
+    {
+      "path": "../codemirror"
+    },
+    {
+      "path": "../notebook"
+    },
+    {
+      "path": "../outputarea"
+    },
+    {
+      "path": "../rendermime"
+    }
+  ]
+}

+ 56 - 0
packages/nbconvert-css/webpack.config.js

@@ -0,0 +1,56 @@
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const path = require('path');
+
+module.exports = {
+  entry: './lib',
+  output: {
+    filename: 'index.js',
+    path: path.resolve(__dirname, 'style')
+  },
+  plugins: [
+    new MiniCssExtractPlugin({
+      filename: 'index.css'
+    })
+  ],
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [
+          {
+            loader: MiniCssExtractPlugin.loader,
+            options: {
+              hmr: process.env.NODE_ENV === 'development'
+            }
+          },
+          'css-loader'
+        ]
+      },
+      {
+        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+        use: 'url-loader'
+      },
+      /* Use null-loader to drop resources that are not used in the CSS */
+      {
+        test: /\.(jpg|png|gif)$/,
+        use: 'null-loader'
+      },
+      {
+        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
+        use: 'null-loader'
+      },
+      {
+        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
+        use: 'null-loader'
+      },
+      {
+        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+        use: 'null-loader'
+      },
+      {
+        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
+        use: 'null-loader'
+      }
+    ]
+  }
+};

+ 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.",

+ 10 - 2
packages/notebook-extension/src/index.ts

@@ -20,7 +20,14 @@ import { CodeCell } from '@jupyterlab/cells';
 
 import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
 
-import { ISettingRegistry, IStateDB, PageConfig } from '@jupyterlab/coreutils';
+import {
+  ISettingRegistry,
+  IStateDB,
+  nbformat,
+  PageConfig,
+  URLExt
+} from '@jupyterlab/coreutils';
+
 
 import { IDocumentManager } from '@jupyterlab/docmanager';
 
@@ -621,7 +628,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'
   };
 
   /**

+ 0 - 4
packages/terminal-extension/src/index.ts

@@ -185,10 +185,6 @@ function activate(
       command: CommandIDs.setTheme,
       args: { theme: 'dark', isPalette: false }
     });
-    mainMenu.settingsMenu.addGroup(
-      [{ type: 'submenu', submenu: themeMenu }],
-      2
-    );
 
     // Add some commands to the "View" menu.
     mainMenu.settingsMenu.addGroup(

+ 197 - 0
tests/test-apputils/src/inputdialog.spec.ts

@@ -0,0 +1,197 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { getItem, getText, getNumber } from '@jupyterlab/apputils';
+
+import {
+  acceptDialog,
+  dismissDialog,
+  waitForDialog
+} from '@jupyterlab/testutils';
+
+describe('@jupyterlab/apputils', () => {
+  describe('getItem()', () => {
+    it('should accept at least two arguments', async () => {
+      const dialog = getItem({
+        label: 'list',
+        items: ['item1']
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getItem({
+        label: 'list',
+        items: ['item1', 'item2'],
+        current: 1,
+        editable: false,
+        title: 'Pick a choice',
+        placeholder: 'item'
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('item2');
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getItem({
+        label: 'list',
+        items: ['item1', 'item2'],
+        title: 'Pick a choice',
+        placeholder: 'item',
+        editable: true,
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = 'item3';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('item3');
+      document.body.removeChild(node);
+    });
+  });
+
+  describe('getText()', () => {
+    it('should accept at least one argument', async () => {
+      const dialog = getText({
+        label: 'text'
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getText({
+        label: 'text',
+        title: 'Give a text',
+        placeholder: 'your text',
+        text: 'answer'
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('answer');
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getText({
+        label: 'text',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = 'my answer';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('my answer');
+      document.body.removeChild(node);
+    });
+  });
+
+  describe('getNumber()', () => {
+    it('should accept at least one argument', async () => {
+      const dialog = getNumber({
+        label: 'number'
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getNumber({
+        label: 'number',
+        title: 'Pick a number',
+        value: 10
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(10);
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getNumber({
+        label: 'text',
+        title: 'Pick a number',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = '25';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(25);
+      document.body.removeChild(node);
+    });
+
+    it('should return NaN if empty', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getNumber({
+        label: 'text',
+        title: 'Pick a number',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = '';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(Number.NaN);
+      document.body.removeChild(node);
+    });
+  });
+});

+ 1 - 1
tests/test-codemirror/package.json

@@ -16,7 +16,7 @@
     "@jupyterlab/codemirror": "^1.0.0-alpha.6",
     "@jupyterlab/testutils": "^1.0.0-alpha.6",
     "chai": "~4.1.2",
-    "codemirror": "~5.42.0",
+    "codemirror": "~5.46.0",
     "jest": "^24.7.1",
     "jest-junit": "^6.3.0",
     "simulate-event": "~1.4.0",

+ 5 - 7
tests/test-filebrowser/src/model.spec.ts

@@ -26,7 +26,8 @@ import {
 import {
   acceptDialog,
   dismissDialog,
-  signalToPromises
+  signalToPromises,
+  sleep
 } from '@jupyterlab/testutils';
 import { toArray } from '@phosphor/algorithm';
 
@@ -259,18 +260,15 @@ describe('filebrowser/model', () => {
           opener,
           manager: delayedServiceManager
         });
-        model = new FileBrowserModel({ manager, state });
+        model = new FileBrowserModel({ manager, state }); // Should delay 1000ms
 
-        const paths: string[] = [];
         // An initial refresh is called in the constructor.
         // If it is too slow, it can come in after the directory change,
         // causing a directory set by, e.g., the tree handler to be wrong.
         // This checks to make sure we are handling that case correctly.
-        const refresh = model.refresh().then(() => paths.push(model.path));
-        const cd = model.cd('src').then(() => paths.push(model.path));
-        await Promise.all([refresh, cd]);
+        await model.cd('src'); // should delay 500ms
+        await sleep(2000);
         expect(model.path).to.equal('src');
-        expect(paths).to.eql(['', 'src']);
 
         manager.dispose();
         delayedServiceManager.contents.dispose();

+ 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

+ 12 - 3
yarn.lock

@@ -3220,9 +3220,10 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
-codemirror@~5.42.0:
-  version "5.42.0"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.42.0.tgz#2d5b640ed009e89dee9ed8a2a778e2a25b65f9eb"
+codemirror@~5.46.0:
+  version "5.46.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.46.0.tgz#be3591572f88911e0105a007c324856a9ece0fb7"
+  integrity sha512-3QpMge0vg4QEhHW3hBAtCipJEWjTJrqLLXdIaWptJOblf1vHFeXLNtFhPai/uX2lnFCehWNk4yOdaMR853Z02w==
 
 collection-visit@^1.0.0:
   version "1.0.0"
@@ -8257,6 +8258,14 @@ null-check@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
 
+null-loader@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-1.0.0.tgz#90e85798e50e9dd1d568495a44e74829dec26744"
+  integrity sha512-mYLDjDVTkjTlFoidxRhzO75rdcwfVXfw5G5zpj8sXnBkHtKJxMk4hTcRR4i5SOhDB6EvcQuYriy6IV23eq6uog==
+  dependencies:
+    loader-utils "^1.2.3"
+    schema-utils "^1.0.0"
+
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"