Quellcode durchsuchen

Merge pull request #1140 from blink1073/code-editor

Abstract Editor Redux
Afshin Darian vor 8 Jahren
Ursprung
Commit
ae83de8cbf
50 geänderte Dateien mit 3104 neuen und 1367 gelöschten Zeilen
  1. 3 3
      examples/console/src/index.ts
  2. 9 1
      examples/filebrowser/src/index.ts
  3. 2 0
      examples/lab/index.js
  4. 3 3
      examples/notebook/src/index.ts
  5. 2 0
      jupyterlab/src/extensions.ts
  6. 1 1
      jupyterlab/src/tsconfig.json
  7. 847 0
      src/codeeditor/editor.ts
  8. 24 0
      src/codeeditor/factory.ts
  9. 42 0
      src/codeeditor/index.ts
  10. 39 0
      src/codeeditor/mimetype.ts
  11. 37 60
      src/codeeditor/widget.ts
  12. 427 0
      src/codemirror/editor.ts
  13. 84 0
      src/codemirror/factory.ts
  14. 5 100
      src/codemirror/index.ts
  15. 57 0
      src/codemirror/mimetype.ts
  16. 100 0
      src/codemirror/mode.ts
  17. 183 0
      src/codemirror/model.ts
  18. 179 0
      src/codemirror/plugin.ts
  19. 51 0
      src/console/codemirror/index.ts
  20. 15 4
      src/console/codemirror/plugin.ts
  21. 0 111
      src/console/codemirror/widget.ts
  22. 112 1
      src/console/content.ts
  23. 18 138
      src/editorwidget/plugin.ts
  24. 56 45
      src/editorwidget/widget.ts
  25. 323 2
      src/notebook/cells/editor.ts
  26. 1 0
      src/notebook/cells/index.ts
  27. 40 4
      src/notebook/cells/widget.ts
  28. 0 322
      src/notebook/codemirror/cells/editor.ts
  29. 0 97
      src/notebook/codemirror/cells/widget.ts
  30. 95 0
      src/notebook/codemirror/index.ts
  31. 0 47
      src/notebook/codemirror/notebook/panel.ts
  32. 0 125
      src/notebook/codemirror/notebook/widget.ts
  33. 20 6
      src/notebook/codemirror/plugin.ts
  34. 0 37
      src/notebook/common/mimetype.ts
  35. 37 2
      src/notebook/notebook/panel.ts
  36. 95 6
      src/notebook/notebook/widget.ts
  37. 1 1
      src/renderers/widget.ts
  38. 13 26
      test/src/completer/handler.spec.ts
  39. 8 4
      test/src/console/content.spec.ts
  40. 3 3
      test/src/console/foreign.spec.ts
  41. 3 3
      test/src/console/panel.spec.ts
  42. 73 73
      test/src/notebook/cells/editor.spec.ts
  43. 20 71
      test/src/notebook/cells/widget.spec.ts
  44. 5 5
      test/src/notebook/notebook/actions.spec.ts
  45. 3 3
      test/src/notebook/notebook/default-toolbar.spec.ts
  46. 12 23
      test/src/notebook/notebook/panel.spec.ts
  47. 12 20
      test/src/notebook/notebook/widget.spec.ts
  48. 5 6
      test/src/notebook/notebook/widgetfactory.spec.ts
  49. 5 5
      test/src/notebook/tracker.spec.ts
  50. 34 9
      typings/codemirror/codemirror.d.ts

+ 3 - 3
examples/console/src/index.ts

@@ -30,8 +30,8 @@ import {
 } from 'jupyterlab/lib/console';
 
 import {
-  CodeMirrorConsoleRenderer
-} from 'jupyterlab/lib/console/codemirror/widget';
+  createRenderer
+} from 'jupyterlab/lib/console/codemirror';
 
 import {
   RenderMime
@@ -107,7 +107,7 @@ function startApp(session: Session.ISession) {
   }
   let sanitizer = defaultSanitizer;
   let rendermime = new RenderMime({ renderers, order, sanitizer });
-  let renderer = CodeMirrorConsoleRenderer.defaultRenderer;
+  let renderer = createRenderer();
 
   let consolePanel = new ConsolePanel({ session, renderer, rendermime });
   consolePanel.title.label = TITLE;

+ 9 - 1
examples/filebrowser/src/index.ts

@@ -41,6 +41,10 @@ import {
   DocumentRegistry, TextModelFactory
 } from 'jupyterlab/lib/docregistry';
 
+import {
+  CodeMirrorEditorFactory, CodeMirrorMimeTypeService
+} from 'jupyterlab/lib/codemirror';
+
 import {
   EditorWidgetFactory
 } from 'jupyterlab/lib/editorwidget/widget';
@@ -87,7 +91,11 @@ function createApp(manager: ServiceManager.IManager): void {
     opener
   });
   let mFactory = new TextModelFactory();
-  let wFactory = new EditorWidgetFactory({
+  let editorServices = {
+    factory: new CodeMirrorEditorFactory(),
+    mimeTypeService: new CodeMirrorMimeTypeService()
+  };
+  let wFactory = new EditorWidgetFactory(editorServices, {
     name: 'Editor',
     modelName: 'text',
     fileExtensions: ['*'],

+ 2 - 0
examples/lab/index.js

@@ -16,6 +16,8 @@ lab.registerPlugins([
   require('jupyterlab/lib/about/plugin').plugin,
   require('jupyterlab/lib/application/plugin').plugin,
   require('jupyterlab/lib/clipboard/plugin').plugin,
+  require('jupyterlab/lib/codemirror/plugin').editorServices,
+  require('jupyterlab/lib/codemirror/plugin').editorCommands,
   require('jupyterlab/lib/commandlinker/plugin').plugin,
   require('jupyterlab/lib/commandpalette/plugin').plugin,
   require('jupyterlab/lib/console/plugin').plugin,

+ 3 - 3
examples/notebook/src/index.ts

@@ -35,8 +35,8 @@ import {
 } from 'jupyterlab/lib/notebook';
 
 import {
-  CodeMirrorNotebookPanelRenderer
-} from 'jupyterlab/lib/notebook/codemirror/notebook/panel';
+  createNotebookPanelRenderer
+} from 'jupyterlab/lib/notebook/codemirror';
 
 import {
   DocumentManager
@@ -142,7 +142,7 @@ function createApp(manager: ServiceManager.IManager): void {
   });
   let mFactory = new NotebookModelFactory();
   let clipboard = new MimeData();
-  let renderer = CodeMirrorNotebookPanelRenderer.defaultRenderer;
+  let renderer = createNotebookPanelRenderer();
   let wFactory = new NotebookWidgetFactory({
     name: 'Notebook',
     modelName: 'notebook',

+ 2 - 0
jupyterlab/src/extensions.ts

@@ -5,6 +5,8 @@ module.exports = [
   require('../../lib/about/plugin').plugin,
   require('../../lib/application/plugin').plugin,
   require('../../lib/clipboard/plugin').plugin,
+  require('../../lib/codemirror/plugin').editorServices,
+  require('../../lib/codemirror/plugin').editorCommands,
   require('../../lib/commandlinker/plugin').plugin,
   require('../../lib/commandpalette/plugin').plugin,
   require('../../lib/console/plugin').plugin,

+ 1 - 1
jupyterlab/src/tsconfig.json

@@ -9,6 +9,6 @@
     "moduleResolution": "node",
     "target": "ES5",
     "outDir": "../build",
-    "sourceMap": true
+    "sourceMap": false
   }
 }

+ 847 - 0
src/codeeditor/editor.ts

@@ -0,0 +1,847 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IDisposable
+} from 'phosphor/lib/core/disposable';
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
+import {
+  ISignal, clearSignalData, defineSignal
+} from 'phosphor/lib/core/signaling';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  IChangedArgs
+} from '../common/interfaces';
+
+
+/**
+ * A namespace for code editors.
+ */
+export
+namespace CodeEditor {
+  /**
+   * A zero-based position in the editor.
+   */
+  export
+  interface IPosition {
+    /**
+     * The cursor line number.
+     */
+    line: number;
+
+    /**
+     * The cursor column number.
+     */
+    column: number;
+  }
+
+  /**
+   * The dimension of an element.
+   */
+  export
+  interface IDimension {
+    /**
+     * The width of an element in pixels.  
+     */
+    width: number;
+
+    /**
+     * The height of an element in pixels.
+     */
+    height: number;
+  }
+
+  /**
+   * An interface describing editor state coordinates.
+   */
+  export
+  interface ICoordinate {
+    /**
+     * The left coordinate value.
+     */
+    readonly left: number;
+
+    /**
+     * The right coordinate value.
+     */
+    readonly right: number;
+
+    /**
+     * The top coordinate value.
+     */
+    readonly top: number;
+
+    /**
+     * The bottom coordinate value.
+     */
+    readonly bottom: number;
+  }
+
+  /**
+   * A range.
+   */
+  export
+  interface IRange {
+    /**
+     * The position of the first character in the current range.
+     *
+     * #### Notes
+     * If this position is greater than [end] then the range is considered
+     * to be backward.
+     */
+    start: IPosition;
+
+    /**
+     * The position of the last character in the current range.
+     *
+     * #### Notes
+     * If this position is less than [start] then the range is considered
+     * to be backward.
+     */
+    end: IPosition;
+  }
+
+  /**
+   * A selection style.
+   */
+  export
+  interface ISelectionStyle {
+    /**
+     * A class name added to a selection.
+     */
+    className?: string;
+
+    /**
+     * A display name added to a selection.
+     */
+    displayName?: string;
+  }
+
+  /**
+   * A text selection.
+   */
+  export
+  interface ITextSelection extends IRange {
+    /**
+     * The uuid of the text selection owner.
+     */
+    uuid: string;
+
+    /**
+     * The style of this selection.
+     */
+    style?: ISelectionStyle;
+  }
+
+  /**
+   * An interface to manage selections by selection owners.
+   * 
+   * #### Definitions
+   * - a user code that has an associated uuid is called a selection owner, see `CodeEditor.ISelectionOwner`
+   * - a selection belongs to a selection owner only if it is associated with the owner by an uuid, see `CodeEditor.ITextSelection`
+   * 
+   * #### Read access
+   * - any user code can observe any selection
+   * 
+   * #### Write access
+   * - if a user code is a selection owner then:
+   *   - it can change selections beloging to it
+   *   - but it must not change selections beloging to other selection owners
+   * - otherwise it must not change any selection
+   */
+  export
+  interface ISelections {
+
+    /**
+     * A signal emitted when selections changes.
+     */
+    readonly changed: ISignal<Selections, ISelections.IChangedArgs>;
+
+    /**
+     * The uuids of selection owners.
+     */
+    readonly uuids: string[];
+
+    /**
+     * Gets the selections for all the cursors in ascending order. 
+     */
+    getSelections(uuid: string): ITextSelection[];
+
+    /**
+     * Sets the selections for all the cursors.
+     */
+    setSelections(uuid: string, newSelections: ITextSelection[]): void;
+  }
+
+  /**
+   * A namespace for `ISelections`.
+   */
+  export
+  namespace ISelections {
+    /**
+     * An arguments for the selection changed signal.
+     */
+    export
+    interface IChangedArgs {
+      /**
+       * The uuid of a selection owner.
+       */
+      readonly uuid: string;
+      /**
+       * The old selections.
+       */
+      readonly oldSelections: ITextSelection[];
+      /**
+       * The new selections.
+       */
+      readonly newSelections: ITextSelection[];
+    }
+  }
+
+  /**
+   * Default implementation of `ISelections`.
+   */
+  export
+  class Selections implements ISelections {
+
+    /**
+     * A signal emitted when selections changes.
+     */
+    readonly changed: ISignal<Selections, ISelections.IChangedArgs>;
+
+    /**
+     * Uuids of all selection owners.
+     */
+    get uuids(): string[] {
+      return Object.keys(this._selections);
+    }
+
+    /**
+     * Gets the selections for all the cursors in ascending order. 
+     */
+    getSelections(uuid: string): ITextSelection[] {
+      const selections = this._selections[uuid];
+      return selections ? selections : [];
+    }
+
+    /**
+     * Sets the selections for all the cursors.
+     */
+    setSelections(uuid: string, newSelections: ITextSelection[]): void {
+      const oldSelections = this.getSelections(uuid);
+      this.removeSelections(uuid);
+      this.sortSelections(newSelections);
+      this._selections[uuid] = newSelections;
+      this.changed.emit({ uuid, oldSelections, newSelections });
+    }
+
+    /**
+     * Sorts given selections in ascending order.
+     */
+    protected sortSelections(selections: ITextSelection[]) {
+      selections.sort((selection, selection2) => {
+        const result = selection.start.line - selection2.start.line;
+        if (result !== 0) {
+          return result;
+        }
+        return selection.start.column - selection2.start.column;
+      });
+    }
+
+    /**
+     * Removes selections by the given uuid.
+     */
+    protected removeSelections(uuid: string) {
+      delete this._selections[uuid];
+    }
+
+    private _selections: {
+      [key: string]: ITextSelection[] | null
+    } = {};
+  }
+
+  defineSignal(Selections.prototype, 'changed');
+
+  /**
+   * An editor model.
+   */
+  export
+  interface IModel extends IDisposable {
+    /**
+     * A signal emitted when the value changes.
+     */
+    valueChanged: ISignal<IModel, IChangedArgs<string>>;
+
+    /**
+     * A signal emitted when a property changes.
+     */
+    mimeTypeChanged: ISignal<IModel, IChangedArgs<string>>;
+
+    /**
+     * The text stored in the model.
+     */
+    value: string;  // TODO: this should be an iobservablestring.
+
+    /**
+     * A mime type of the model.
+     * 
+     * #### Notes
+     * It is never `null`, the default mime type is `text/plain`.
+     */
+    mimeType: string;
+
+    /**
+     * The currently selected code.
+     */
+    readonly selections: ISelections;
+
+    /**
+     * Get the number of lines in the model.
+     */
+    readonly lineCount: number;
+
+    /**
+     * Returns the content for the given line number.
+     */
+    getLine(line: number): string;
+
+    /**
+     * Find an offset for the given position.
+     */
+    getOffsetAt(position: IPosition): number;
+
+    /**
+     * Find a position for the given offset.
+     */
+    getPositionAt(offset: number): IPosition;
+
+    /**
+     * Undo one edit (if any undo events are stored).
+     */
+    undo(): void;
+
+    /**
+     * Redo one undone edit.
+     */
+    redo(): void;
+
+    /**
+     * Clear the undo history.
+     */
+    clearHistory(): void;
+  }
+
+  /**
+   * A selection owner.
+   */
+  export
+  interface ISelectionOwner {
+    /**
+     * The uuid of this selection owner.
+     */
+    readonly uuid: string;
+    /**
+     * Returns the primary position of the cursor, never `null`.
+     */
+    getCursorPosition(): IPosition;
+    /**
+     * Set the primary position of the cursor. This will remove any secondary cursors.
+     */
+    setCursorPosition(position: IPosition): void;
+    /**
+     * Returns the primary selection, never `null`.
+     */
+    getSelection(): IRange;
+    /**
+     * Set the primary selection. This will remove any secondary cursors.
+     */
+    setSelection(selection: IRange): void;
+    /**
+     * Gets the selections for all the cursors, never `null` or empty.
+     */
+    getSelections(): IRange[];
+    /**
+     * Sets the selections for all the cursors.
+     * Cursors will be removed or added, as necessary.
+     * Passing an empty array resets a cursor position to the start of a document.
+     */
+    setSelections(selections: IRange[]): void;
+  }
+
+  /**
+   * A keydown handler type.
+   * 
+   * #### Notes
+   * Return `true` to prevent the default handling of the event by the
+   * editor.
+   */
+  export
+  type KeydownHandler = (instance: IEditor, event: KeyboardEvent) => boolean;
+
+  /**
+   * A widget that provides a code editor.
+   */
+  export
+  interface IEditor extends ISelectionOwner, IDisposable {
+    /**
+     * Whether line numbers should be displayed. Defaults to false.
+     */
+    lineNumbers: boolean;
+
+    /**
+     * Set to false for horizontal scrolling. Defaults to true.
+     */
+    wordWrap: boolean;
+
+    /**
+     * Whether the editor is read-only.  Defaults to false.
+     */
+    readOnly: boolean;
+
+    /**
+     * The model used by the editor.
+     */
+    readonly model: IModel;
+
+    /**
+     * The height of a line in the editor in pixels.
+     */
+    readonly lineHeight: number;
+
+    /**
+     * The widget of a character in the editor in pixels.
+     */
+    readonly charWidth: number;
+
+    /**
+     * Handle keydown events for the editor.
+     */
+    onKeyDown: KeydownHandler | null;
+
+    /**
+     * Brings browser focus to this editor text.
+     */
+    focus(): void;
+
+    /**
+     * Test whether the editor has keyboard focus.
+     */
+    hasFocus(): boolean;
+
+    /**
+     * Repaint editor. 
+     */
+    refresh(): void;
+
+    /**
+     * Sets the size of the editor.
+     * 
+     * #### Notes
+     * Sets null if the size is unknown.
+     */
+    setSize(size: IDimension | null): void;
+
+    /**
+     * Reveals the given position in the editor.
+     */
+    revealPosition(position: IPosition): void;
+
+    /**
+     * Reveals the given selection in the editor.
+     */
+    revealSelection(selection: IRange): void;
+
+    /**
+     * Get the window coordinates given a cursor position.
+     */
+    getCoordinate(position: IPosition): ICoordinate;
+  }
+
+  /**
+   * The options used to initialize an editor.
+   */
+  export
+  interface IOptions {
+    /**
+     * Whether line numbers should be displayed. Defaults to false.
+     */
+    lineNumbers?: boolean;
+
+    /**
+     * Set to false for horizontal scrolling. Defaults to true.
+     */
+    wordWrap?: boolean;
+
+    /**
+     * Whether the editor is read-only. Defaults to false.
+     */
+    readOnly?: boolean;
+
+    /**
+     * Extra options.
+     */
+    extra?: { [key: string]: any };
+  }
+
+  /**
+   * The default implementation of the code editor model.
+   */
+  export
+  class Model implements IModel {
+    /**
+     * A signal emitted when a content of the model changed.
+     */
+    valueChanged: ISignal<this, IChangedArgs<string>>;
+
+    /**
+     * A signal emitted when a mimetype changes.
+     */
+    mimeTypeChanged: ISignal<this, IChangedArgs<string>>;
+
+    readonly selections = new Selections();
+
+    /**
+     * Whether the model is disposed.
+     */
+    get isDisposed(): boolean {
+      return this._isDisposed;
+    }
+
+    /**
+     * Dipose of the resources used by the model.
+     */
+    dispose(): void {
+      if (this._isDisposed) {
+        return;
+      }
+      this._isDisposed = true;
+      clearSignalData(this);
+    }
+
+    /**
+     * A mime type of the model.
+     */
+    get mimeType(): string {
+      return this._mimetype;
+    }
+    set mimeType(newValue: string) {
+      let oldValue = this._mimetype;
+      if (oldValue === newValue) {
+        return;
+      }
+      this._mimetype = newValue;
+      this.mimeTypeChanged.emit({
+        name: 'mimeType',
+        oldValue,
+        newValue
+      });
+    }
+
+    /**
+     * The text stored in the model.
+     */
+    get value(): string {
+      return this._value;
+    }
+    set value(newValue: string) {
+      let oldValue = this._value;
+      if (oldValue === newValue) {
+        return;
+      }
+      this._value = newValue;
+      this.valueChanged.emit({
+        name: 'value',
+        oldValue,
+        newValue
+      });
+    }
+
+    /**
+     * Get the number of lines in the model.
+     */
+    get lineCount(): number {
+      return this._value.split('\n').length;
+    }
+
+    /**
+     * Returns the content for the given line number.
+     */
+    getLine(line: number): string {
+      return this._value.split('\n')[line];
+    }
+
+    /**
+     * Find an offset for the given position.
+     */
+    getOffsetAt(position: CodeEditor.IPosition): number {
+      let lines = this._value.split('\n');
+      let before = lines.slice(0, position.line).join('\n').length;
+      return before + position.column;
+    }
+
+    /**
+     * Find a position fot the given offset.
+     */
+    getPositionAt(offset: number): CodeEditor.IPosition {
+      let text = this._value.slice(0, offset);
+      let lines = text.split('\n');
+      let column = lines[lines.length - 1].length;
+      return { line: lines.length - 1, column };
+    }
+
+    /**
+     * Undo one edit (if any undo events are stored).
+     */
+    undo(): void { /* no-op */ }
+
+    /**
+     * Redo one undone edit.
+     */
+    redo(): void { /* no-op */ }
+
+    /**
+     * Clear the undo history.
+     */
+    clearHistory(): void { /* no-op */ }
+
+    private _mimetype = '';
+    private _value = '';
+    private _isDisposed = false;
+  }
+}
+
+defineSignal(CodeEditor.Model.prototype, 'valueChanged');
+defineSignal(CodeEditor.Model.prototype, 'mimeTypeChanged');
+
+
+/**
+ * An implementation of an editor for an html text area.
+ */
+class TextAreaEditor extends Widget implements CodeEditor.IEditor {
+  readonly uuid = '1';
+  /**
+   * Construct a new text editor.
+   */
+  constructor(options: CodeEditor.IOptions, model: CodeEditor.IModel) {
+    super({ node: document.createElement('textarea') });
+    this._model = model;
+    let node = this.node as HTMLTextAreaElement;
+    node.readOnly = options.readOnly || false;
+    node.wrap = options.wordWrap ? 'hard' : 'soft';
+    model.selections.changed.connect(this.onModelSelectionsChanged, this);
+    model.valueChanged.connect(this.onModelValueChanged, this);
+    this.updateSelections();
+  }
+
+  get lineNumbers(): boolean {
+    return false;
+  }
+  set lineNumbers(value: boolean) {
+    /* no-op*/
+  }
+
+  /**
+   * Set to false for horizontal scrolling. Defaults to true.
+   */
+  get wordWrap(): boolean {
+    return (this.node as HTMLTextAreaElement).wrap === 'hard';
+  }
+  set wordWrap(value: boolean) {
+    (this.node as HTMLTextAreaElement).wrap = value ? 'hard' : 'soft';
+  }
+
+  /**
+   * Whether the editor is read-only.  Defaults to false.
+   */
+  get readOnly(): boolean {
+    return (this.node as HTMLTextAreaElement).readOnly;
+  }
+  set readOnly(value: boolean) {
+    (this.node as HTMLTextAreaElement).readOnly = value;
+  }
+
+  get model(): CodeEditor.IModel {
+    return this._model;
+  }
+
+  get onKeyDown(): CodeEditor.KeydownHandler | null {
+    return this._handler;
+  }
+  set onKeyDown(value: CodeEditor.KeydownHandler | null) {
+    this._handler = value;
+  }
+
+  get charWidth(): number {
+    // TODO css measurement
+    return -1;
+  }
+
+  get lineHeight(): number {
+    // TODO css measurement
+    return -1;
+  }
+
+  /**
+   * Brings browser focus to this editor text.
+   */
+  focus(): void {
+    this.node.focus();
+  }
+
+  /**
+   * Test whether the editor has keyboard focus.
+   */
+  hasFocus(): boolean {
+    return document.activeElement === this.node;
+  }
+
+  /**
+   * Repaint the editor.
+   */
+  refresh(): void { /* no-op */ }
+
+  /**
+   * Set the size of the editor in pixels.
+   */
+  setSize(dimension: CodeEditor.IDimension | null): void {
+    // override css here
+  }
+
+  /**
+   * Scroll the given cursor position into view.
+   */
+  revealPosition(pos: CodeEditor.IPosition): void {
+    // set node scroll position here.
+  }
+
+  /**
+   * Scroll the given cursor position into view.
+   */
+  revealSelection(selection: CodeEditor.IRange): void {
+    // set node scroll position here.
+  }
+
+  /**
+   * Get the window coordinates given a cursor position.
+   */
+  getCoordinate(position: CodeEditor.IPosition): CodeEditor.ICoordinate {
+    // more css measurements required
+    return void 0;
+  }
+
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'keydown':
+      this._evtKeydown(event as KeyboardEvent);
+      break;
+    case 'mouseup':
+      this._evtMouseUp(event as MouseEvent);
+      break;
+    case 'input':
+      this._evtInput(event);
+      break;
+    default:
+      break;
+    }
+  }
+
+  protected onAfterAttach(msg: Message): void {
+    super.onAfterAttach(msg);
+    this.node.addEventListener('keydown', this);
+    this.node.addEventListener('mouseup', this);
+    this.node.addEventListener('input', this);
+  }
+
+  protected onBeforeDetach(msg: Message): void {
+    this.node.removeEventListener('keydown', this);
+    this.node.removeEventListener('mouseup', this);
+    this.node.removeEventListener('input', this);
+  }
+
+  protected onModelValueChanged(sender: CodeEditor.IModel, args: IChangedArgs<string>): void {
+    if (this._changeGuard) {
+      return;
+    }
+    (this.node as HTMLTextAreaElement).value = args.newValue;
+  }
+
+  setCursorPosition(position: CodeEditor.IPosition): void {
+    this.setSelection({
+      start: position,
+      end: position
+    });
+  }
+
+  getCursorPosition(): CodeEditor.IPosition {
+    return this.getSelection().start;
+  }
+
+  setSelection(selection: CodeEditor.IRange | null): void {
+    const node = this.node as HTMLTextAreaElement;
+    if (selection) {
+      const start = this.model.getOffsetAt(selection.start);
+      const end = this.model.getOffsetAt(selection.end);
+      node.setSelectionRange(start, end);
+    } else {
+      node.setSelectionRange(0, 0);
+    }
+  }
+
+  getSelection(): CodeEditor.ITextSelection {
+    const node = this.node as HTMLTextAreaElement;
+    return {
+      uuid: this.uuid,
+      start: this.model.getPositionAt(node.selectionStart),
+      end: this.model.getPositionAt(node.selectionEnd)
+    };
+  }
+
+  getSelections(): CodeEditor.ITextSelection[] {
+    return [this.getSelection()];
+  }
+
+  setSelections(selections: CodeEditor.IRange[]): void {
+    this.setSelection(selections.length > 0 ? selections[0] : null);
+  }
+
+  protected onModelSelectionsChanged(sender: CodeEditor.ISelections, args: CodeEditor.ISelections.IChangedArgs) {
+    // display foreign cursors
+  }
+
+  private _evtKeydown(event: KeyboardEvent): void {
+    let handler = this._handler;
+    if (handler) {
+      handler(this, event);
+    }
+  }
+
+  private _evtMouseUp(event: MouseEvent): void {
+    this.updateSelections();
+  }
+
+  protected updateSelections() {
+    this._changeGuard = true;
+    const selections = this.getSelections();
+    this._model.selections.setSelections(this.uuid, selections);
+    this._changeGuard = false;
+  }
+
+  private _evtInput(event: Event): void {
+    let node = this.node as HTMLTextAreaElement;
+    this._changeGuard = true;
+    this.model.value = node.value;
+    this._changeGuard = false;
+  }
+
+  private _model: CodeEditor.IModel;
+  private _handler: CodeEditor.KeydownHandler | null = null;
+  private _changeGuard = false;
+}

+ 24 - 0
src/codeeditor/factory.ts

@@ -0,0 +1,24 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  CodeEditor
+} from './editor';
+
+/**
+ * The editor factory interface.
+ */
+export
+interface IEditorFactory {
+
+  /**
+   * Create a new editor for inline code.
+   */
+  newInlineEditor(host: HTMLElement, options: CodeEditor.IOptions): CodeEditor.IEditor;
+
+  /**
+   * Create a new editor for a full document.
+   */
+  newDocumentEditor(host: HTMLElement, options: CodeEditor.IOptions): CodeEditor.IEditor;
+
+}

+ 42 - 0
src/codeeditor/index.ts

@@ -0,0 +1,42 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Token
+} from 'phosphor/lib/core/token';
+
+import {
+  IEditorFactory
+} from './factory';
+
+import {
+  IEditorMimeTypeService
+} from './mimetype';
+
+export * from './editor';
+export * from './widget';
+export * from './factory';
+export * from './mimetype';
+
+/* tslint:disable */
+/**
+ * Code editor services token.
+ */
+export
+const IEditorServices = new Token<IEditorServices>('jupyter.services.editorservices');
+/* tslint:enable */
+
+/**
+ * Code editor services.
+ */
+export
+interface IEditorServices {
+  /**
+   * The code editor factory.
+   */
+  readonly factory: IEditorFactory;
+  /**
+   * The editor mime type service.
+   */
+  readonly mimeTypeService: IEditorMimeTypeService;
+}

+ 39 - 0
src/codeeditor/mimetype.ts

@@ -0,0 +1,39 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  nbformat
+} from '@jupyterlab/services';
+
+/**
+ * The mime type service of a code editor.
+ */
+export
+interface IEditorMimeTypeService {
+  /**
+   * Returns a mime type for the given language info.
+   * 
+   * #### Notes
+   * If a mime type cannot be found returns the defaul mime type `text/plain`, never `null`.  
+   */
+  getMimeTypeByLanguage(info: nbformat.ILanguageInfoMetadata): string;
+  /**
+   * Returns a mime type for the given file path.
+   * 
+   * #### Notes
+   * If a mime type cannot be found returns the defaul mime type `text/plain`, never `null`.
+   */
+  getMimeTypeByFilePath(filePath: string): string;
+}
+
+/**
+ * A namespace for `IEditorMimeTypeService`.
+ */
+export
+namespace IEditorMimeTypeService {
+  /**
+   * The default mime type.
+   */
+  export
+  const defaultMimeType = 'text/plain';
+}

+ 37 - 60
src/codemirror/widget.ts → src/codeeditor/widget.ts

@@ -1,11 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import * as CodeMirror
-  from 'codemirror';
-
-import 'codemirror/mode/meta';
-
 import {
   Message
 } from 'phosphor/lib/core/messaging';
@@ -14,78 +9,56 @@ import {
   ResizeMessage, Widget
 } from 'phosphor/lib/ui/widget';
 
+import {
+  CodeEditor
+} from './';
 
 /**
- * The class name added to CodeMirrorWidget instances.
- */
-const EDITOR_CLASS = 'jp-CodeMirrorWidget';
-
-/**
- * The name of the default CodeMirror theme
+ * A widget which hosts a code editor.
  */
 export
-const DEFAULT_CODEMIRROR_THEME = 'jupyter';
-
+class CodeEditorWidget extends Widget {
 
-/**
- * A widget which hosts a CodeMirror editor.
- */
-export
-class CodeMirrorWidget extends Widget {
+  constructor(editorFactory: (host: Widget) => CodeEditor.IEditor) {
+    super();
+    this._editor = editorFactory(this);
+  }
 
   /**
-   * Construct a CodeMirror widget.
+   * Get the editor wrapped by the widget.
+   *
+   * #### Notes
+   * This is a ready-only property.
    */
-  constructor(options: CodeMirror.EditorConfiguration = {}) {
-    super();
-    this.addClass(EDITOR_CLASS);
-    options.theme = (options.theme || DEFAULT_CODEMIRROR_THEME);
-    this._editor = CodeMirror(this.node, options);
+   get editor(): CodeEditor.IEditor {
+    return this._editor;
   }
 
   /**
    * Dispose of the resources held by the widget.
    */
-  dispose(): void {
+  dispose() {
+    if (this.isDisposed) {
+      return;
+    }
     clearTimeout(this._resizing);
-    this._editor = null;
     super.dispose();
+    this._editor.dispose();
+    this._editor = null;
   }
 
   /**
-   * Get the editor wrapped by the widget.
-   *
-   * #### Notes
-   * This is a ready-only property.
-   */
-   get editor(): CodeMirror.Editor {
-     return this._editor;
-   }
-
-  /**
-   * Handle the DOM events for the widget.
-   *
-   * @param event - The DOM event sent to the widget.
-   *
-   * #### Notes
-   * This method implements the DOM `EventListener` interface and is
-   * called in response to events on the notebook panel's node. It should
-   * not be called directly by user code.
+   * Handle `'activate-request'` messages.
    */
-  handleEvent(event: Event): void {
-    switch (event.type) {
-    case 'focus':
-      this._evtFocus(event as FocusEvent);
-      break;
-    default:
-      break;
-    }
+  protected onActivateRequest(msg: Message): void {
+    this._editor.focus();
   }
 
   /**
    * A message handler invoked on an `'after-attach'` message.
    */
   protected onAfterAttach(msg: Message): void {
+    super.onAfterAttach(msg);
     this.node.addEventListener('focus', this, true);
     if (!this.isVisible) {
       this._needsRefresh = true;
@@ -119,21 +92,24 @@ class CodeMirrorWidget extends Widget {
     if (msg.width < 0 || msg.height < 0) {
       if (this._resizing === -1) {
         this._resizing = setTimeout(() => {
-          this._editor.setSize(null, null);
+          this._editor.setSize(null);
           this._resizing = -1;
         }, 500);
       }
     } else {
-      this._editor.setSize(msg.width, msg.height);
+      this._editor.setSize(msg);
     }
     this._needsRefresh = true;
   }
 
-  /**
-   * Handle `'activate-request'` messages.
-   */
-  protected onActivateRequest(msg: Message): void {
-    this._editor.focus();
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'focus':
+      this._evtFocus(event as FocusEvent);
+      break;
+    default:
+      break;
+    }
   }
 
   /**
@@ -146,7 +122,8 @@ class CodeMirrorWidget extends Widget {
     }
   }
 
-  private _editor: CodeMirror.Editor = null;
+  private _editor: CodeEditor.IEditor = null;
   private _needsRefresh = true;
   private _resizing = -1;
+
 }

+ 427 - 0
src/codemirror/editor.ts

@@ -0,0 +1,427 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import * as CodeMirror
+  from 'codemirror';
+
+import {
+  IChangedArgs
+} from '../common/interfaces';
+
+import {
+  loadModeByMIME
+} from './';
+
+import {
+  CodeEditor
+} from '../codeeditor';
+
+import {
+  CodeMirrorModel
+} from './model';
+
+/**
+ * The class name added to CodeMirrorWidget instances.
+ */
+const EDITOR_CLASS = 'jp-CodeMirrorWidget';
+
+/**
+ * The name of the default CodeMirror theme
+ */
+export
+const DEFAULT_CODEMIRROR_THEME = 'jupyter';
+
+/**
+ * CodeMirror editor.
+ */
+export
+class CodeMirrorEditor implements CodeEditor.IEditor {
+
+  /**
+   * The uuid of this editor;
+   */
+  readonly uuid: string;
+
+  /**
+   * The selection style of this editor.
+   */
+  readonly selectionStyle?: CodeEditor.ISelectionStyle;
+
+  /**
+   * Handle keydown events for the editor.
+   */
+  onKeyDown: CodeEditor.KeydownHandler | null = null;
+
+  /**
+   * Construct a CodeMirror editor.
+   */
+  constructor(host: HTMLElement, options: CodeMirrorEditor.IOptions) {
+    host.classList.add(EDITOR_CLASS);
+    this.uuid = this.uuid;
+    this.selectionStyle = this.selectionStyle;
+
+    this._model = new CodeMirrorModel();
+
+    options.theme = (options.theme || DEFAULT_CODEMIRROR_THEME);
+    options.value = this._model.doc;
+    this._editor = CodeMirror(host, options);
+
+    this._model.mimeTypeChanged.connect((model, args) => this._onMimeTypeChanged(model, args));
+    this._model.selections.changed.connect((selections, args) => this._onSelectionsChanged(selections, args));
+
+    CodeMirror.on(this.editor, 'keydown', (editor, event) => this._onKeyDown(editor, event));
+    CodeMirror.on(this.editor, 'cursorActivity', () => this._onCursorActivity());
+  }
+
+  /**
+   * Tests whether the editor is disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+    // FIXME: dispose selections
+    this._editor = null;
+  }
+
+  /**
+   * Get the editor wrapped by the widget.
+   *
+   * #### Notes
+   * This is a ready-only property.
+   */
+  get editor(): CodeMirror.Editor {
+    return this._editor;
+  }
+
+  /**
+   * Control the rendering of line numbers.
+   */
+  get lineNumbers(): boolean {
+    return this._editor.getOption('lineNumbers');
+  }
+  set lineNumbers(value: boolean) {
+    this._editor.setOption('lineNumbers', value);
+  }
+
+  /**
+   * Set to false for horizontal scrolling. Defaults to true.
+   */
+  get wordWrap(): boolean {
+    return this._editor.getOption('wordWrap');
+  }
+  set wordWrap(value: boolean) {
+    this._editor.setOption('wordWrap', value);
+  }
+
+  /**
+   * Should the editor be read only.
+   */
+  get readOnly(): boolean {
+    return this._editor.getOption('readOnly') === 'nocursor';
+  }
+  set readOnly(readOnly: boolean) {
+    let option = readOnly ? 'nocursor' : false;
+    this._editor.setOption('readOnly', option);
+  }
+
+  /**
+   * Returns a model for this editor.
+   */
+  get model(): CodeEditor.IModel {
+    return this._model;
+  }
+
+  /**
+   * The height of a line in the editor in pixels.
+   */
+  get lineHeight(): number {
+    return this._editor.defaultTextHeight();
+  }
+
+  /**
+   * The widget of a character in the editor in pixels.
+   */
+  get charWidth(): number {
+    return this._editor.defaultCharWidth();
+  }
+
+  /**
+   * Brings browser focus to this editor text.
+   */
+  focus(): void {
+    this._editor.focus();
+  }
+
+  /**
+   * Test whether the editor has keyboard focus.
+   */
+  hasFocus(): boolean {
+    return this._editor.hasFocus();
+  }
+
+  /**
+   * Repaint editor.
+   */
+  refresh(): void {
+    this._editor.refresh();
+  }
+
+  /**
+   * Set the size of the editor in pixels.
+   */
+  setSize(dimension: CodeEditor.IDimension | null): void {
+    if (dimension) {
+      this._editor.setSize(dimension.width, dimension.height);
+    } else {
+      this._editor.setSize(null, null);
+    }
+  }
+
+  /**
+   * Reveal the given position in the editor.
+   */
+  revealPosition(position: CodeEditor.IPosition): void {
+    const cmPosition = this.toCodeMirrorPosition(position);
+    this._editor.scrollIntoView(cmPosition);
+  }
+
+  /**
+   * Reveal the given selection in the editor.
+   */
+  revealSelection(selection: CodeEditor.IRange): void {
+    const range = this.toCodeMirrorRange(selection);
+    this._editor.scrollIntoView(range);
+  }
+
+  /**
+   * Get the window coordinates given a cursor position.
+   */
+  getCoordinate(position: CodeEditor.IPosition): CodeEditor.ICoordinate {
+    return this.editor.charCoords(this.toCodeMirrorPosition(position), 'page');
+  }
+
+  /**
+   * Returns the primary position of the cursor, never `null`.
+   */
+  getCursorPosition(): CodeEditor.IPosition {
+    const cursor = this._model.doc.getCursor();
+    return this.toPosition(cursor);
+  }
+
+  /**
+   * Set the primary position of the cursor. This will remove any secondary cursors.
+   */
+  setCursorPosition(position: CodeEditor.IPosition): void {
+    const cursor = this.toCodeMirrorPosition(position);
+    this._model.doc.setCursor(cursor);
+  }
+
+  /**
+   * Returns the primary selection, never `null`.
+   */
+  getSelection(): CodeEditor.ITextSelection {
+    return this.getSelections()[0];
+  }
+
+  /**
+   * Set the primary selection. This will remove any secondary cursors.
+   */
+  setSelection(selection: CodeEditor.IRange): void {
+    this.setSelections([selection]);
+  }
+
+  /**
+   * Gets the selections for all the cursors, never `null` or empty.
+   */
+  getSelections(): CodeEditor.ITextSelection[] {
+    const selections = this._model.doc.listSelections();
+    if (selections.length > 0) {
+      return this._model.doc.listSelections().map(selection => this.toSelection(selection));
+    }
+    const cursor = this._model.doc.getCursor();
+    const selection = this.toSelection({ anchor: cursor, head: cursor });
+    return [selection];
+  }
+
+  /**
+   * Sets the selections for all the cursors, should not be empty.
+   * Cursors will be removed or added, as necessary.
+   * Passing an empty array resets a cursor position to the start of a document.
+   */
+  setSelections(selections: CodeEditor.IRange[]): void {
+    const cmSelections = this.toCodeMirrorSelections(selections);
+    this._model.doc.setSelections(cmSelections, 0);
+  }
+
+  /**
+   * Converts selections to code mirror selections.
+   */
+  protected toCodeMirrorSelections(selections: CodeEditor.IRange[]): CodeMirror.Selection[] {
+    if (selections.length > 0) {
+      return selections.map(selection => this.toCodeMirrorSelection(selection));
+    }
+    const position = { line: 0, ch: 0 };
+    return [{ anchor: position, head: position }];
+  }
+
+  /**
+   * Handles a mime type change.
+   */
+  protected _onMimeTypeChanged(model: CodeMirrorModel, args: IChangedArgs<string>): void {
+      const mime = args.newValue;
+      loadModeByMIME(this._editor, mime);
+  }
+
+  /**
+   * Handles a selections change.
+   */
+  protected _onSelectionsChanged(selections: CodeEditor.ISelections, args: CodeEditor.ISelections.IChangedArgs): void {
+    const uuid = args.uuid;
+    if (uuid !== this.uuid) {
+      this.cleanSelections(uuid);
+      this.markSelections(uuid, args.newSelections);
+    }
+  }
+
+  /**
+   * Clean selections for the given uuid.
+   */
+  protected cleanSelections(uuid: string) {
+    const markers = this.selectionMarkers[uuid];
+    if (markers) {
+      markers.forEach(marker => marker.clear());
+    }
+    delete this.selectionMarkers[uuid];
+  }
+
+  /**
+   * Marks selections.
+   */
+  protected markSelections(uuid: string, selections: CodeEditor.ITextSelection[]) {
+    const markers: CodeMirror.TextMarker[] = [];
+    for (const selection of selections) {
+      const { anchor, head } = this.toCodeMirrorSelection(selection);
+      const markerOptions = this.toTextMarkerOptions(selection);
+      this._model.doc.markText(anchor, head, markerOptions);
+    }
+    this.selectionMarkers[uuid] = markers;
+  }
+
+  /**
+   * Handles a key down event.
+   */
+  protected _onKeyDown(editor: CodeMirror.Editor, event: KeyboardEvent): void {
+    if (this.onKeyDown && this.onKeyDown(this, event)) {
+      event.preventDefault();
+    }
+  }
+
+  /**
+   * Handles a cursor activity event.
+   */
+  protected _onCursorActivity(): void {
+    const selections = this.getSelections();
+    this.model.selections.setSelections(this.uuid, selections);
+  }
+
+  /**
+   * Converts a code mirror selectio to an editor selection.
+   */
+  protected toSelection(selection: CodeMirror.Selection): CodeEditor.ITextSelection {
+    return {
+      uuid: this.uuid,
+      start: this.toPosition(selection.anchor),
+      end: this.toPosition(selection.head),
+      style: this.selectionStyle
+    };
+  }
+
+  /**
+   * Converts the selection style to a text marker options.
+   */
+  protected toTextMarkerOptions(style: CodeEditor.ISelectionStyle | undefined): CodeMirror.TextMarkerOptions | undefined {
+    if (style) {
+      return {
+        className: style.className,
+        title: style.displayName
+      };
+    }
+    return undefined;
+  }
+
+  /**
+   * Converts an editor selection to a code mirror selection.
+   */
+  protected toCodeMirrorSelection(selection: CodeEditor.IRange): CodeMirror.Selection {
+    return {
+      anchor: this.toCodeMirrorPosition(selection.start),
+      head: this.toCodeMirrorPosition(selection.end)
+    };
+  }
+
+  /**
+   * Converts an editor selection to a code mirror selection.
+   */
+  protected toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range {
+    return {
+      from: this.toCodeMirrorPosition(range.start),
+      to: this.toCodeMirrorPosition(range.end)
+    };
+  }
+
+  /**
+   * Convert a code mirror position to an editor position.
+   */
+  protected toPosition(position: CodeMirror.Position) {
+    return {
+      line: position.line,
+      column: position.ch
+    };
+  }
+
+  /**
+   * Convert an editor position to a code mirror position.
+   */
+  protected toCodeMirrorPosition(position: CodeEditor.IPosition) {
+    return {
+      line: position.line,
+      ch: position.column
+    };
+  }
+
+  private _model: CodeMirrorModel;
+  private _editor: CodeMirror.Editor;
+  private _isDisposed = false;
+  protected selectionMarkers: { [key: string]: CodeMirror.TextMarker[] | undefined } = {};
+
+}
+
+/**
+ * A namespace for `CodeMirrorEditor`.
+ */
+export
+namespace CodeMirrorEditor {
+  /**
+   * An initialization options for a code mirror editor.
+   */
+  export
+  interface IOptions extends CodeMirror.EditorConfiguration {
+    /**
+     * The uuid of an editor.
+     */
+    readonly uuid: string;
+    /**
+     * A selection style.
+     */
+    readonly selectionStyle?: CodeEditor.ISelectionStyle;
+  }
+}

+ 84 - 0
src/codemirror/factory.ts

@@ -0,0 +1,84 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  utils
+} from '@jupyterlab/services';
+
+import {
+  CodeEditor, IEditorFactory
+} from '../codeeditor';
+
+import {
+  CodeMirrorEditor, DEFAULT_CODEMIRROR_THEME
+} from './editor';
+
+/**
+ * CodeMirror editor factory.
+ */
+export
+class CodeMirrorEditorFactory implements IEditorFactory {
+
+  /**
+   * Create a new editor for inline code.
+   */
+  newInlineEditor(host: HTMLElement, options: CodeEditor.IOptions): CodeEditor.IEditor {
+    return this.newEditor(host, {
+      uuid: utils.uuid(),
+      indentUnit: 4,
+      theme: DEFAULT_CODEMIRROR_THEME,
+      extraKeys: {
+        'Cmd-Right': 'goLineRight',
+        'End': 'goLineRight',
+        'Cmd-Left': 'goLineLeft',
+        'Tab': 'indentMore',
+        'Shift-Tab': 'indentLess',
+        'Cmd-Alt-[': 'indentAuto',
+        'Ctrl-Alt-[': 'indentAuto',
+        'Cmd-/': 'toggleComment',
+        'Ctrl-/': 'toggleComment',
+      }
+    }, options);
+  }
+
+  /**
+   * Create a new editor for a full document.
+   */
+  newDocumentEditor(host: HTMLElement, options: CodeEditor.IOptions): CodeEditor.IEditor {
+    return this.newEditor(host, {
+      uuid: utils.uuid(),
+      extraKeys: {
+        'Tab': 'indentMore',
+        'Shift-Enter': () => { /* no-op */ }
+      },
+      indentUnit: 4,
+      theme: DEFAULT_CODEMIRROR_THEME,
+      lineNumbers: true,
+      lineWrapping: true
+    }, options);
+  }
+
+  /**
+   * Creates an editor and applies extra options.
+   */
+  protected newEditor(host: HTMLElement, editorOptions: CodeMirrorEditor.IOptions, options: CodeEditor.IOptions) {
+    if (options.readOnly !== undefined) {
+      editorOptions.readOnly = options.readOnly;
+    }
+    if (options.lineNumbers !== undefined) {
+      editorOptions.lineNumbers = options.lineNumbers;
+    }
+    if (options.wordWrap !== undefined) {
+      editorOptions.lineWrapping = options.wordWrap;
+    }
+    const editor = new CodeMirrorEditor(host, editorOptions);
+    const extra = options.extra;
+    if (extra) {
+      for (const option in extra) {
+        editor.editor.setOption(option, extra[option]);
+      }
+    }
+    return editor;
+  }
+
+}

+ 5 - 100
src/codemirror/index.ts

@@ -1,100 +1,5 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import * as CodeMirror
-  from 'codemirror';
-
-import 'codemirror/mode/meta';
-
-import './codemirror-ipython';
-import './codemirror-ipythongfm';
-
-// Bundle other common modes
-import 'codemirror/mode/javascript/javascript';
-import 'codemirror/mode/css/css';
-import 'codemirror/mode/julia/julia';
-import 'codemirror/mode/r/r';
-import 'codemirror/mode/markdown/markdown';
-
-
-// Stub for the require function.
-declare var require: any;
-
-
-/**
- * Load a codemirror mode by file name.
- */
-export
-function loadModeByFileName(editor: CodeMirror.Editor, filename: string): void {
-  loadInfo(editor, CodeMirror.findModeByFileName(filename));
-}
-
-
-/**
- * Load a codemirror mode by mime type.
- */
-export
-function loadModeByMIME(editor: CodeMirror.Editor, mimetype: string): void {
-  loadInfo(editor, CodeMirror.findModeByMIME(mimetype));
-}
-
-
-/**
- * Load a codemirror mode by mode name.
- */
-export
-function loadModeByName(editor: CodeMirror.Editor, mode: string): void {
-  loadInfo(editor, CodeMirror.findModeByName(mode));
-}
-
-
-/**
- * Find a codemirror mode by name or CodeMirror spec.
- */
-export
-function findMode(mode: string | CodeMirror.modespec): CodeMirror.modespec {
-  let modename = (typeof mode === 'string') ? mode :
-      mode.mode || mode.name;
-  let mimetype = (typeof mode !== 'string') ? mode.mime : '';
-
-  return (
-    CodeMirror.findModeByName(modename) ||
-    CodeMirror.findModeByMIME(mimetype) ||
-    CodeMirror.modes['null']
-  );
-}
-
-
-/**
- * Require a codemirror mode by name or Codemirror spec.
- */
-export
-function requireMode(mode: string | CodeMirror.modespec): Promise<CodeMirror.modespec> {
-  let info = findMode(mode);
-
-  // Simplest, cheapest check by mode name.
-  if (CodeMirror.modes.hasOwnProperty(info.mode)) {
-    return Promise.resolve(info);
-  }
-
-  // Fetch the mode asynchronously.
-  return new Promise<CodeMirror.modespec>((resolve, reject) => {
-    require([`codemirror/mode/${info.mode}/${info.mode}`], () => {
-      resolve(info);
-    });
-  });
-}
-
-
-/**
- * Load a CodeMirror mode based on a mode spec.
- */
-function loadInfo(editor: CodeMirror.Editor, info: CodeMirror.modespec): void {
-  if (!info) {
-    editor.setOption('mode', 'null');
-    return;
-  }
-  requireMode(info).then(() => {
-    editor.setOption('mode', info.mime);
-  });
-}
+export * from './mode';
+export * from './model';
+export * from './editor';
+export * from './factory';
+export * from './mimetype';

+ 57 - 0
src/codemirror/mimetype.ts

@@ -0,0 +1,57 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import * as CodeMirror
+  from 'codemirror';
+
+import {
+  nbformat
+} from '@jupyterlab/services';
+
+import {
+  IEditorMimeTypeService
+} from '../codeeditor';
+
+import {
+  findMode
+} from './index';
+
+/**
+ * The mime type service for CodeMirror.
+ */
+export
+class CodeMirrorMimeTypeService implements IEditorMimeTypeService {
+  /**
+   * Returns a mime type for the given language info.
+   *
+   * #### Notes
+   * If a mime type cannot be found returns the defaul mime type `text/plain`, never `null`.
+   */
+  getMimeTypeByLanguage(info: nbformat.ILanguageInfoMetadata): string {
+    if (info.codemirror_mode) {
+      return findMode(info.codemirror_mode as any).mime;
+    }
+    let mode = CodeMirror.findModeByMIME(info.mimetype || '');
+    if (mode) {
+      return info.mimetype!;
+    }
+    let ext = info.file_extension || '';
+    ext = ext.split('.').slice(-1)[0];
+    mode = CodeMirror.findModeByExtension(ext || '');
+    if (mode) {
+      return mode.mime;
+    }
+    mode = CodeMirror.findModeByName(info.name || '');
+    return mode ? mode.mime : IEditorMimeTypeService.defaultMimeType;
+  }
+  /**
+   * Returns a mime type for the given file path.
+   *
+   * #### Notes
+   * If a mime type cannot be found returns the defaul mime type `text/plain`, never `null`.
+   */
+  getMimeTypeByFilePath(path: string): string {
+    const mode = CodeMirror.findModeByFileName(path);
+    return mode ? mode.mime : IEditorMimeTypeService.defaultMimeType;
+  }
+}

+ 100 - 0
src/codemirror/mode.ts

@@ -0,0 +1,100 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import * as CodeMirror
+  from 'codemirror';
+
+import 'codemirror/mode/meta';
+
+import './codemirror-ipython';
+import './codemirror-ipythongfm';
+
+// Bundle other common modes
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/css/css';
+import 'codemirror/mode/julia/julia';
+import 'codemirror/mode/r/r';
+import 'codemirror/mode/markdown/markdown';
+
+
+// Stub for the require function.
+declare var require: any;
+
+
+/**
+ * Load a codemirror mode by file name.
+ */
+export
+function loadModeByFileName(editor: CodeMirror.Editor, filename: string): void {
+  loadInfo(editor, CodeMirror.findModeByFileName(filename));
+}
+
+
+/**
+ * Load a codemirror mode by mime type.
+ */
+export
+function loadModeByMIME(editor: CodeMirror.Editor, mimetype: string): void {
+  loadInfo(editor, CodeMirror.findModeByMIME(mimetype));
+}
+
+
+/**
+ * Load a codemirror mode by mode name.
+ */
+export
+function loadModeByName(editor: CodeMirror.Editor, mode: string): void {
+  loadInfo(editor, CodeMirror.findModeByName(mode));
+}
+
+
+/**
+ * Find a codemirror mode by name or CodeMirror spec.
+ */
+export
+function findMode(mode: string | CodeMirror.modespec): CodeMirror.modespec {
+  let modename = (typeof mode === 'string') ? mode :
+      mode.mode || mode.name;
+  let mimetype = (typeof mode !== 'string') ? mode.mime : '';
+
+  return (
+    CodeMirror.findModeByName(modename) ||
+    CodeMirror.findModeByMIME(mimetype) ||
+    CodeMirror.modes['null']
+  );
+}
+
+
+/**
+ * Require a codemirror mode by name or Codemirror spec.
+ */
+export
+function requireMode(mode: string | CodeMirror.modespec): Promise<CodeMirror.modespec> {
+  let info = findMode(mode);
+
+  // Simplest, cheapest check by mode name.
+  if (CodeMirror.modes.hasOwnProperty(info.mode)) {
+    return Promise.resolve(info);
+  }
+
+  // Fetch the mode asynchronously.
+  return new Promise<CodeMirror.modespec>((resolve, reject) => {
+    require([`codemirror/mode/${info.mode}/${info.mode}`], () => {
+      resolve(info);
+    });
+  });
+}
+
+
+/**
+ * Load a CodeMirror mode based on a mode spec.
+ */
+function loadInfo(editor: CodeMirror.Editor, info: CodeMirror.modespec): void {
+  if (!info) {
+    editor.setOption('mode', 'null');
+    return;
+  }
+  requireMode(info).then(() => {
+    editor.setOption('mode', info.mime);
+  });
+}

+ 183 - 0
src/codemirror/model.ts

@@ -0,0 +1,183 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import * as CodeMirror
+  from 'codemirror';
+
+import {
+  ISignal, clearSignalData, defineSignal
+} from 'phosphor/lib/core/signaling';
+
+import {
+  CodeEditor
+} from '../codeeditor/editor';
+
+import {
+  IChangedArgs
+} from '../common/interfaces';
+
+
+/**
+ * An implementation of the code editor model using code mirror.
+ */
+export
+  class CodeMirrorModel implements CodeEditor.IModel {
+
+  /**
+   * A signal emitted when a content of the model changed.
+   */
+  readonly valueChanged: ISignal<this, IChangedArgs<string>>;
+
+  /**
+   * A signal emitted when a mimetype changes.
+   */
+  readonly mimeTypeChanged: ISignal<this, IChangedArgs<string>>;
+
+  /**
+   * Get the selections for the model.
+   */
+  readonly selections: CodeEditor.ISelections = new CodeEditor.Selections();
+
+  /**
+   * Construct a new codemirror model.
+   */
+  constructor(doc: CodeMirror.Doc = new CodeMirror.Doc('')) {
+    this._doc = doc;
+    this._value = this.value;
+    CodeMirror.on(this.doc, 'change', (instance, change) => {
+      this._onDocChange(instance, change);
+    });
+  }
+
+  get doc(): CodeMirror.Doc {
+    return this._doc;
+  }
+
+  /**
+   * Whether the model is disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Dipose of the resources used by the model.
+   */
+  dispose(): void {
+    if (this._isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+    clearSignalData(this);
+  }
+
+  /**
+   * A mime type of the model.
+   */
+  get mimeType(): string {
+    return this._mimetype;
+  }
+  set mimeType(newValue: string) {
+    const oldValue = this._mimetype;
+    if (oldValue === newValue) {
+      return;
+    }
+    this._mimetype = newValue;
+    this.mimeTypeChanged.emit({
+      name: 'mimeType',
+      oldValue,
+      newValue
+    });
+  }
+
+  /**
+   * The text stored in the model.
+   */
+  get value(): string {
+    return this._doc.getValue();
+  }
+  set value(value: string) {
+    this._doc.setValue(value);
+  }
+
+  /**
+   * Get the number of lines in the model.
+   */
+  get lineCount(): number {
+    return this._doc.lineCount();
+  }
+
+  /**
+   * Returns the content for the given line number.
+   */
+  getLine(line: number): string {
+    return this._doc.getLine(line);
+  }
+
+  /**
+   * Find an offset for the given position.
+   */
+  getOffsetAt(position: CodeEditor.IPosition): number {
+    return this._doc.indexFromPos({
+      ch: position.column,
+      line: position.line
+    });
+  }
+
+  /**
+   * Find a position fot the given offset.
+   */
+  getPositionAt(offset: number): CodeEditor.IPosition {
+    const { ch, line } = this._doc.posFromIndex(offset);
+    return { line, column: ch };
+  }
+
+  /**
+   * Undo one edit (if any undo events are stored).
+   */
+  undo(): void {
+    this._doc.undo();
+  }
+
+  /**
+   * Redo one undone edit.
+   */
+  redo(): void {
+    this._doc.redo();
+  }
+
+  /**
+   * Clear the undo history.
+   */
+  clearHistory(): void {
+    this._doc.clearHistory();
+  }
+
+  /**
+   * Handles document changes.
+   */
+  protected _onDocChange(doc: CodeMirror.Doc, change: CodeMirror.EditorChange) {
+    const oldValue = this._value;
+    const newValue = this.value;
+    if (oldValue !== newValue) {
+      this._value = newValue;
+      this.valueChanged.emit({
+        name: 'value',
+        oldValue,
+        newValue
+      });
+    }
+  }
+
+  private _mimetype = '';
+  /**
+   * A snapshot of a document value before the change, see `_onDocChange`
+   */
+  private _value: string;
+  private _isDisposed = false;
+  private _doc: CodeMirror.Doc;
+}
+
+
+defineSignal(CodeMirrorModel.prototype, 'valueChanged');
+defineSignal(CodeMirrorModel.prototype, 'mimeTypeChanged');

+ 179 - 0
src/codemirror/plugin.ts

@@ -0,0 +1,179 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import 'codemirror/addon/edit/matchbrackets.js';
+import 'codemirror/addon/edit/closebrackets.js';
+import 'codemirror/addon/comment/comment.js';
+import 'codemirror/keymap/vim.js';
+
+import {
+  Menu
+} from 'phosphor/lib/ui/menu';
+
+import {
+  JupyterLab, JupyterLabPlugin
+} from '../application';
+
+import {
+  IEditorServices
+} from '../codeeditor';
+
+import {
+  ICommandPalette
+} from '../commandpalette';
+
+import {
+  IEditorTracker
+} from '../editorwidget';
+
+import {
+  IMainMenu
+} from '../mainmenu';
+
+import {
+  CodeMirrorEditorFactory, CodeMirrorMimeTypeService, CodeMirrorEditor, DEFAULT_CODEMIRROR_THEME
+} from '.';
+
+
+/**
+ * The editor services.
+ */
+export
+const editorServices: JupyterLabPlugin<IEditorServices> = {
+  id: IEditorServices.name,
+  provides: IEditorServices,
+  activate: (): IEditorServices => {
+    const factory = new CodeMirrorEditorFactory();
+    const mimeTypeService = new CodeMirrorMimeTypeService();
+    return { factory, mimeTypeService };
+  }
+};
+
+
+/**
+ * The editor commands.
+ */
+export
+const editorCommands: JupyterLabPlugin<void> = {
+  id: 'jupyter.services.editor-commands',
+  requires: [IEditorTracker, IMainMenu, ICommandPalette],
+  activate: activateEditorCommands,
+  autoStart: true
+};
+
+
+/**
+ * The map of command ids used by the editor.
+ */
+const cmdIds = {
+  lineNumbers: 'editor:line-numbers',
+  lineWrap: 'editor:line-wrap',
+  matchBrackets: 'editor:match-brackets',
+  vimMode: 'editor:vim-mode',
+  changeTheme: 'editor:change-theme',
+  createConsole: 'editor:create-console',
+  runCode: 'editor:run-code'
+};
+
+
+/**
+ * Set up the editor widget menu and commands.
+ */
+function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMenu: IMainMenu, palette: ICommandPalette): void {
+  let { commands, keymap } = app;
+
+  /**
+   * Toggle editor matching brackets
+   */
+  function toggleMatchBrackets(): void {
+    if (tracker.currentWidget) {
+      let editor = tracker.currentWidget.editor;
+      if (editor instanceof CodeMirrorEditor) {
+        let cm = editor.editor;
+        cm.setOption('matchBrackets', !cm.getOption('matchBrackets'));
+      }
+    }
+  }
+
+  /**
+   * Toggle the editor's vim mode
+   */
+  function toggleVim(): void {
+    tracker.forEach(widget => {
+      if (widget.editor instanceof CodeMirrorEditor) {
+        let cm = widget.editor.editor;
+        let keymap = cm.getOption('keyMap') === 'vim' ? 'default'
+        : 'vim';
+        cm.setOption('keyMap', keymap);
+      }
+    });
+  }
+
+  /**
+   * Create a menu for the editor.
+   */
+  function createMenu(): Menu {
+    let settings = new Menu({ commands, keymap });
+    let theme = new Menu({ commands, keymap });
+    let menu = new Menu({ commands, keymap });
+
+    menu.title.label = 'Editor';
+    settings.title.label = 'Settings';
+    theme.title.label = 'Theme';
+
+    settings.addItem({ command: cmdIds.lineNumbers });
+    settings.addItem({ command: cmdIds.lineWrap });
+    settings.addItem({ command: cmdIds.matchBrackets });
+    settings.addItem({ command: cmdIds.vimMode });
+
+    commands.addCommand(cmdIds.changeTheme, {
+      label: args => args['theme'] as string,
+      execute: args => {
+        let name: string = args['theme'] as string || DEFAULT_CODEMIRROR_THEME;
+        tracker.forEach(widget => {
+          if (widget.editor instanceof CodeMirrorEditor) {
+            let cm = widget.editor.editor;
+            cm.setOption('theme', name);
+          }
+        });
+      }
+    });
+
+    [
+     'jupyter', 'default', 'abcdef', 'base16-dark', 'base16-light',
+     'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix',
+     'xq-light', 'zenburn'
+    ].forEach(name => theme.addItem({
+      command: 'editor:change-theme',
+      args: { theme: name }
+    }));
+
+    menu.addItem({ type: 'separator' });
+    menu.addItem({ type: 'submenu', menu: settings });
+    menu.addItem({ type: 'submenu', menu: theme });
+
+    return menu;
+  }
+
+  mainMenu.addMenu(createMenu(), { rank: 30 });
+
+  commands.addCommand(cmdIds.matchBrackets, {
+    execute: () => { toggleMatchBrackets(); },
+    label: 'Toggle Match Brackets',
+  });
+
+  commands.addCommand(cmdIds.vimMode, {
+    execute: () => { toggleVim(); },
+    label: 'Toggle Vim Mode'
+  });
+
+  [
+    cmdIds.lineNumbers,
+    cmdIds.lineWrap,
+    cmdIds.matchBrackets,
+    cmdIds.vimMode,
+    cmdIds.createConsole,
+    cmdIds.runCode,
+  ].forEach(command => palette.addItem({ command, category: 'Editor' }));
+
+}

+ 51 - 0
src/console/codemirror/index.ts

@@ -0,0 +1,51 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  ConsoleContent
+} from '../content';
+
+import {
+  IEditorServices
+} from '../../codeeditor';
+
+import {
+  CodeMirrorEditorFactory, CodeMirrorMimeTypeService
+} from '../../codemirror';
+
+import {
+  CodeCellWidget
+} from '../../notebook/cells';
+
+
+/**
+ * Create a console renderer given editor services.
+ */
+export
+function createRenderer(editorServices?: IEditorServices): ConsoleContent.IRenderer {
+  editorServices = editorServices || {
+    factory: new CodeMirrorEditorFactory(),
+    mimeTypeService: new CodeMirrorMimeTypeService()
+  };
+  const bannerRenderer = new CodeCellWidget.Renderer({
+    editorFactory: host => editorServices.factory.newInlineEditor(host.node, {
+      wordWrap: true
+    })
+  });
+  const promptRenderer = new CodeCellWidget.Renderer({
+    editorFactory: host => editorServices.factory.newInlineEditor(host.node, {
+      extra: {
+        matchBrackets: false,
+        autoCloseBrackets: false,
+        extraKeys: {
+          Enter: function () { /* no-op */ }
+        }
+      }
+    })
+  });
+  const foreignCellRenderer = promptRenderer;
+  const editorMimeTypeService = editorServices.mimeTypeService;
+  return new ConsoleContent.Renderer({
+    bannerRenderer, promptRenderer, foreignCellRenderer, editorMimeTypeService
+  });
+}

+ 15 - 4
src/console/codemirror/plugin.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  JupyterLabPlugin
+  JupyterLab, JupyterLabPlugin
 } from '../../application';
 
 import {
@@ -10,9 +10,12 @@ import {
 } from '../content';
 
 import {
-  CodeMirrorConsoleRenderer
-} from './widget';
+  IEditorServices
+} from '../../codeeditor';
 
+import {
+  createRenderer
+} from '.';
 
 /**
  * The provider for a console's code mirror renderer.
@@ -20,6 +23,14 @@ import {
 export
 const plugin: JupyterLabPlugin<ConsoleContent.IRenderer> = {
   id: 'jupyter.services.console.codemirror.renderer',
+  requires: [IEditorServices],
   provides: ConsoleContent.IRenderer,
-  activate: () => CodeMirrorConsoleRenderer.defaultRenderer
+  activate: activateRendererProvider
 };
+
+/**
+ * Activates the renderer provider extension.
+ */
+function activateRendererProvider(app: JupyterLab, editorServices: IEditorServices): ConsoleContent.IRenderer {
+  return createRenderer(editorServices);
+}

+ 0 - 111
src/console/codemirror/widget.ts

@@ -1,111 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  nbformat
-} from '@jupyterlab/services';
-
-import {
-  CodeCellModel, RawCellModel
-} from '../../notebook/cells/model';
-
-import {
-  CodeCellWidget, RawCellWidget
-} from '../../notebook/cells/widget';
-
-import {
-  CodeMirrorCodeCellWidgetRenderer
-} from '../../notebook/codemirror/cells/widget';
-
-import {
-  CodeMirrorNotebookRenderer
-} from '../../notebook/codemirror/notebook/widget';
-
-import {
-  mimetypeForLanguage
-} from '../../notebook/common/mimetype';
-
-import {
-  RenderMime
-} from '../../rendermime';
-
-import {
-  ConsoleContent
-} from '../content';
-
-
-/**
- * A code mirror renderer for a console.
- */
-export
-class CodeMirrorConsoleRenderer implements ConsoleContent.IRenderer {
-  /**
-   * Create a new banner widget.
-   */
-  createBanner(): RawCellWidget {
-    let widget = new RawCellWidget({
-      renderer: CodeMirrorNotebookRenderer.defaultRawCellRenderer
-    });
-    widget.model = new RawCellModel();
-    return widget;
-  }
-
-  /**
-   * Create a new prompt widget.
-   */
-  createPrompt(rendermime: RenderMime, context: ConsoleContent): CodeCellWidget {
-    let widget = new CodeCellWidget({
-      rendermime,
-      renderer: CodeMirrorConsoleRenderer.defaultCodeCellRenderer
-    });
-    widget.model = new CodeCellModel();
-    return widget;
-  }
-
-  /**
-   * Create a new code cell widget for an input from a foreign session.
-   */
-  createForeignCell(rendermime: RenderMime, context: ConsoleContent): CodeCellWidget {
-    let widget = new CodeCellWidget({
-      rendermime,
-      renderer: CodeMirrorConsoleRenderer.defaultCodeCellRenderer
-    });
-    widget.model = new CodeCellModel();
-    return widget;
-  }
-
-  /**
-   * Get the preferred mimetype given language info.
-   */
-  getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string {
-    return mimetypeForLanguage(info);
-  }
-}
-
-
-/**
- * A namespace for `CodeMirrorConsoleRenderer` statics.
- */
-export
-namespace CodeMirrorConsoleRenderer {
-  /**
-   * A default code mirror renderer for a console.
-   */
-  export
-  const defaultRenderer = new CodeMirrorConsoleRenderer();
-
-
-  /**
-   * A default code mirror renderer for a code cell editor.
-   */
-  export
-  const defaultCodeCellRenderer = new CodeMirrorCodeCellWidgetRenderer({
-    editorInitializer: (editor) => {
-      editor.editor.setOption('matchBrackets', false);
-      editor.editor.setOption('autoCloseBrackets', false);
-      editor.editor.setOption('extraKeys', {
-        Enter: function() { /* no-op */ }
-      });
-    }
-  });
-}

+ 112 - 1
src/console/content.ts

@@ -38,7 +38,12 @@ import {
 } from '../inspector';
 
 import {
-  BaseCellWidget, CodeCellWidget, RawCellWidget
+  IEditorMimeTypeService
+} from '../codeeditor';
+
+import {
+  BaseCellWidget, CodeCellWidget, RawCellWidget,
+  CodeCellModel, RawCellModel
 } from '../notebook/cells';
 
 import {
@@ -703,6 +708,112 @@ namespace ConsoleContent {
     getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string;
   }
 
+  /**
+   * Default implementation of `IRenderer`.
+   */
+  export
+  class Renderer implements IRenderer {
+
+    /**
+     * The banner renderer.
+     */
+    readonly bannerRenderer: CodeCellWidget.IRenderer;
+    /**
+     * The prompt renderer.
+     */
+    readonly promptRenderer: CodeCellWidget.IRenderer;
+    /**
+     * The foreign cell renderer.
+     */
+    readonly foreignCellRenderer: CodeCellWidget.IRenderer;
+    /**
+     * The mime type service of a code editor.
+     */
+    readonly editorMimeTypeService: IEditorMimeTypeService;
+
+    /**
+     * Create a new renderer.
+     */
+    constructor(options: Renderer.IOptions) {
+      this.bannerRenderer = options.bannerRenderer;
+      this.promptRenderer = options.promptRenderer;
+      this.foreignCellRenderer = options.foreignCellRenderer;
+      this.editorMimeTypeService = options.editorMimeTypeService;
+    }
+
+    /**
+     * Create a new banner widget.
+     */
+    createBanner(): RawCellWidget {
+      let widget = new RawCellWidget({
+        renderer: this.bannerRenderer
+      });
+      widget.model = new RawCellModel();
+      return widget;
+    }
+
+    /**
+     * Create a new prompt widget.
+     */
+    createPrompt(rendermime: IRenderMime, context: ConsoleContent): CodeCellWidget {
+      let widget = new CodeCellWidget({
+        rendermime,
+        renderer: this.promptRenderer
+      });
+      widget.model = new CodeCellModel();
+      return widget;
+    }
+
+    /**
+     * Create a new code cell widget for an input from a foreign session.
+     */
+    createForeignCell(rendermime: IRenderMime, context: ConsoleContent): CodeCellWidget {
+      let widget = new CodeCellWidget({
+        rendermime,
+        renderer: this.foreignCellRenderer
+      });
+      widget.model = new CodeCellModel();
+      return widget;
+    }
+
+    /**
+     * Get the preferred mimetype given language info.
+     */
+    getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string {
+      return this.editorMimeTypeService.getMimeTypeByLanguage(info);
+    }
+
+  }
+
+  /**
+   * The namespace for `Renderer`.
+   */
+  export
+  namespace Renderer {
+    /**
+     * An initialize options for `Renderer`.
+     */
+    export
+    interface IOptions {
+      /**
+       * The banner renderer.
+       */
+      readonly bannerRenderer: CodeCellWidget.IRenderer;
+      /**
+       * The prompt renderer.
+       */
+      readonly promptRenderer: CodeCellWidget.IRenderer;
+      /**
+       * The foreign cell renderer.
+       */
+      readonly foreignCellRenderer: CodeCellWidget.IRenderer;
+      /**
+       * The mime type service of a code editor.
+       */
+      readonly editorMimeTypeService: IEditorMimeTypeService;
+    }
+  }
+
   /* tslint:disable */
   /**
    * The console renderer token.

+ 18 - 138
src/editorwidget/plugin.ts

@@ -1,31 +1,14 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import 'codemirror/addon/edit/matchbrackets.js';
-import 'codemirror/addon/edit/closebrackets.js';
-import 'codemirror/addon/comment/comment.js';
-import 'codemirror/keymap/vim.js';
-
 import {
   AttachedProperty
 } from 'phosphor/lib/core/properties';
 
-import {
-  Menu
-} from 'phosphor/lib/ui/menu';
-
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
 
-import {
-  DEFAULT_CODEMIRROR_THEME
-} from '../codemirror/widget';
-
-import {
-  ICommandPalette
-} from '../commandpalette';
-
 import {
   InstanceTracker
 } from '../common/instancetracker';
@@ -38,10 +21,6 @@ import {
   ILayoutRestorer
 } from '../layoutrestorer';
 
-import {
-  IMainMenu
-} from '../mainmenu';
-
 import {
   IStateDB
 } from '../statedb';
@@ -50,6 +29,9 @@ import {
   IEditorTracker, EditorWidget, EditorWidgetFactory
 } from './widget';
 
+import {
+  IEditorServices
+} from '../codeeditor';
 
 /**
  * The class name for all main area portrait tab icons.
@@ -66,16 +48,13 @@ const EDITOR_ICON_CLASS = 'jp-ImageTextEditor';
  */
 const FACTORY = 'Editor';
 
+
 /**
  * The map of command ids used by the editor.
  */
 const cmdIds = {
   lineNumbers: 'editor:line-numbers',
   lineWrap: 'editor:line-wrap',
-  matchBrackets: 'editor:match-brackets',
-  vimMode: 'editor:vim-mode',
-  closeAll: 'editor:close-all',
-  changeTheme: 'editor:change-theme',
   createConsole: 'editor:create-console',
   runCode: 'editor:run-code'
 };
@@ -88,7 +67,7 @@ export
 const plugin: JupyterLabPlugin<IEditorTracker> = {
   id: 'jupyter.services.editor-handler',
   requires: [
-    IDocumentRegistry, IMainMenu, ICommandPalette, IStateDB, ILayoutRestorer
+    IDocumentRegistry, IStateDB, ILayoutRestorer, IEditorServices
   ],
   provides: IEditorTracker,
   activate: activateEditorHandler,
@@ -99,8 +78,8 @@ const plugin: JupyterLabPlugin<IEditorTracker> = {
 /**
  * Sets up the editor widget
  */
-function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): IEditorTracker {
-  const factory = new EditorWidgetFactory({
+function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB, layout: ILayoutRestorer, editorServices: IEditorServices): IEditorTracker {
+  const factory = new EditorWidgetFactory(editorServices, {
     name: FACTORY,
     fileExtensions: ['*'],
     defaultFor: ['*']
@@ -130,20 +109,13 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
   });
   registry.addWidgetFactory(factory);
 
-  /**
-   * An attached property for the session id associated with an editor widget.
-   */
-  const sessionIdProperty = new AttachedProperty<EditorWidget, string>({
-    name: 'sessionId'
-  });
-
   /**
    * Toggle editor line numbers
    */
   function toggleLineNums() {
     if (tracker.currentWidget) {
       let editor = tracker.currentWidget.editor;
-      editor.setOption('lineNumbers', !editor.getOption('lineNumbers'));
+      editor.lineNumbers = !editor.lineNumbers;
     }
   }
 
@@ -153,82 +125,16 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
   function toggleLineWrap() {
     if (tracker.currentWidget) {
       let editor = tracker.currentWidget.editor;
-      editor.setOption('lineWrapping', !editor.getOption('lineWrapping'));
+      editor.wordWrap = !editor.wordWrap;
     }
   }
 
   /**
-   * Toggle editor matching brackets
-   */
-  function toggleMatchBrackets() {
-    if (tracker.currentWidget) {
-      let editor = tracker.currentWidget.editor;
-      editor.setOption('matchBrackets', !editor.getOption('matchBrackets'));
-    }
-  }
-
-  /**
-   * Toggle the editor's vim mode
-   */
-  function toggleVim() {
-    tracker.forEach(widget => {
-      let keymap = widget.editor.getOption('keyMap') === 'vim' ? 'default'
-        : 'vim';
-      widget.editor.setOption('keyMap', keymap);
-    });
-  }
-
-  /**
-   * Close all currently open text editor files
-   */
-  function closeAllFiles() {
-    tracker.forEach(widget => { widget.close(); });
-  }
-
-  /**
-   * Create a menu for the editor.
+   * An attached property for the session id associated with an editor widget.
    */
-  function createMenu(app: JupyterLab): Menu {
-    let { commands, keymap } = app;
-    let settings = new Menu({ commands, keymap });
-    let theme = new Menu({ commands, keymap });
-    let menu = new Menu({ commands, keymap });
-
-    menu.title.label = 'Editor';
-    settings.title.label = 'Settings';
-    theme.title.label = 'Theme';
-
-    settings.addItem({ command: cmdIds.lineNumbers });
-    settings.addItem({ command: cmdIds.lineWrap });
-    settings.addItem({ command: cmdIds.matchBrackets });
-    settings.addItem({ command: cmdIds.vimMode });
-
-    commands.addCommand(cmdIds.changeTheme, {
-      label: args => args['theme'] as string,
-      execute: args => {
-        let name: string = args['theme'] as string || DEFAULT_CODEMIRROR_THEME;
-        tracker.forEach(widget => { widget.editor.setOption('theme', name); });
-      }
-    });
-
-    [
-     'jupyter', 'default', 'abcdef', 'base16-dark', 'base16-light',
-     'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix',
-     'xq-light', 'zenburn'
-    ].forEach(name => theme.addItem({
-      command: 'editor:change-theme',
-      args: { theme: name }
-    }));
-
-    menu.addItem({ command: cmdIds.closeAll });
-    menu.addItem({ type: 'separator' });
-    menu.addItem({ type: 'submenu', menu: settings });
-    menu.addItem({ type: 'submenu', menu: theme });
-
-    return menu;
-  }
-
-  mainMenu.addMenu(createMenu(app), {rank: 30});
+  const sessionIdProperty = new AttachedProperty<EditorWidget, string>({
+    name: 'sessionId'
+  });
 
   let commands = app.commands;
 
@@ -242,21 +148,6 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
     label: 'Toggle Line Wrap',
   });
 
-  commands.addCommand(cmdIds.matchBrackets, {
-    execute: () => { toggleMatchBrackets(); },
-    label: 'Toggle Match Brackets',
-  });
-
-  commands.addCommand(cmdIds.vimMode, {
-    execute: () => { toggleVim(); },
-    label: 'Toggle Vim Mode'
-  });
-
-  commands.addCommand(cmdIds.closeAll, {
-    execute: () => { closeAllFiles(); },
-    label: 'Close all files'
-  });
-
   commands.addCommand(cmdIds.createConsole, {
     execute: () => {
       let widget = tracker.currentWidget;
@@ -286,26 +177,15 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
         return;
       }
       // Get the selected code from the editor.
-      let doc = widget.editor.getDoc();
-      let code = doc.getSelection();
-      if (!code) {
-        let { line } = doc.getCursor();
-        code = doc.getLine(line);
-      }
+      const editorModel = widget.editor.model;
+      const selection = widget.editor.getSelection();
+      const start = editorModel.getOffsetAt(selection.start);
+      const end = editorModel.getOffsetAt(selection.end);
+      const code = editorModel.value.substring(start, end);
       commands.execute('console:inject', { id, code });
     },
     label: 'Run Code',
   });
 
-  [
-    cmdIds.lineNumbers,
-    cmdIds.lineWrap,
-    cmdIds.matchBrackets,
-    cmdIds.vimMode,
-    cmdIds.closeAll,
-    cmdIds.createConsole,
-    cmdIds.runCode,
-  ].forEach(command => palette.addItem({ command, category: 'Editor' }));
-
   return tracker;
 }

+ 56 - 45
src/editorwidget/widget.ts

@@ -1,31 +1,29 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import * as CodeMirror
-  from 'codemirror';
-
-import 'codemirror/mode/meta';
-
 import {
   Token
 } from 'phosphor/lib/core/token';
 
-import {
-  loadModeByFileName
-} from '../codemirror';
-
 import {
   IInstanceTracker
 } from '../common/instancetracker';
 
-import {
-  CodeMirrorWidget, DEFAULT_CODEMIRROR_THEME
-} from '../codemirror/widget';
-
 import {
   ABCWidgetFactory, DocumentRegistry
 } from '../docregistry';
 
+import {
+  CodeEditor, IEditorServices, IEditorMimeTypeService
+} from '../codeeditor';
+
+import {
+  CodeEditorWidget
+} from '../codeeditor/widget';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
 
 /**
  * The class name added to a dirty widget.
@@ -50,41 +48,38 @@ interface IEditorTracker extends IInstanceTracker<EditorWidget> {}
  * The editor tracker token.
  */
 export
-const IEditorTracker = new Token<IEditorTracker>('jupyter.services.editor-tracker');
+const IEditorTracker = new Token<EditorWidget>('jupyter.services.editor-tracker');
 /* tslint:enable */
 
 
 /**
- * A document widget for codemirrors.
+ * A document widget for editors.
  */
 export
-class EditorWidget extends CodeMirrorWidget {
+class EditorWidget extends CodeEditorWidget {
   /**
    * Construct a new editor widget.
    */
-  constructor(context: DocumentRegistry.Context) {
-    super({
-      extraKeys: {
-        'Tab': 'indentMore',
-        'Shift-Enter': () => { /* no-op */ }
-      },
-      indentUnit: 4,
-      theme: DEFAULT_CODEMIRROR_THEME,
-      lineNumbers: true,
-      lineWrapping: true,
-    });
+  constructor(
+    editorFactory: (host: Widget) => CodeEditor.IEditor,
+    context: DocumentRegistry.Context,
+    editorMimeTypeService: IEditorMimeTypeService) {
+    super(editorFactory);
     this.addClass(EDITOR_CLASS);
     this._context = context;
-    let editor = this.editor;
     let model = context.model;
-    let doc = editor.getDoc();
+    let editor = this.editor;
+
+
     // Prevent the initial loading from disk from being in the editor history.
     context.ready.then( () => {
-      doc.setValue(model.toString());
-      doc.clearHistory();
+      editor.model.value = model.toString();
+      editor.model.clearHistory();
     });
+
+    editor.model.value = model.toString();
+
     this.title.label = context.path.split('/').pop();
-    loadModeByFileName(editor, context.path);
     model.stateChanged.connect((m, args) => {
       if (args.name === 'dirty') {
         if (args.newValue) {
@@ -94,22 +89,23 @@ class EditorWidget extends CodeMirrorWidget {
         }
       }
     });
-    context.pathChanged.connect((c, path) => {
-      loadModeByFileName(editor, path);
-      this.title.label = path.split('/').pop();
-    });
     model.contentChanged.connect(() => {
-      let old = doc.getValue();
+      let old = editor.model.value;
       let text = model.toString();
       if (old !== text) {
-        doc.setValue(text);
+        editor.model.value = text;
       }
     });
-    CodeMirror.on(doc, 'change', (instance, change) => {
-      if (change.origin !== 'setValue') {
-        model.fromString(instance.getValue());
-      }
+    this.editor.model.valueChanged.connect((sender, args) => {
+      model.fromString(args.newValue);
     });
+    editor.model.mimeType = editorMimeTypeService.getMimeTypeByFilePath(context.path);
+    context.pathChanged.connect((c, path) => {
+      editor.model.mimeType = editorMimeTypeService.getMimeTypeByFilePath(path);
+      this.title.label = path.split('/').pop();
+    });
+
+    // TODO disconnect on deactivation
   }
 
   /**
@@ -119,19 +115,34 @@ class EditorWidget extends CodeMirrorWidget {
     return this._context;
   }
 
-  private _context: DocumentRegistry.Context;
+  protected _context: DocumentRegistry.Context;
 }
 
-
 /**
  * A widget factory for editors.
  */
 export
 class EditorWidgetFactory extends ABCWidgetFactory<EditorWidget, DocumentRegistry.IModel> {
+
+  constructor(editorServices: IEditorServices, options: DocumentRegistry.IWidgetFactoryOptions) {
+    super(options);
+    this._editorServices = editorServices;
+  }
+
   /**
    * Create a new widget given a context.
    */
   protected createNewWidget(context: DocumentRegistry.Context): EditorWidget {
-    return new EditorWidget(context);
+    const { factory, mimeTypeService } = this._editorServices;
+    return new EditorWidget((host: Widget) => {
+      let editor = factory.newDocumentEditor(host.node, {
+          lineNumbers: true,
+          readOnly: false,
+          wordWrap: true,
+      });
+      return editor;
+    }, context, mimeTypeService);
   }
+
+  private _editorServices: IEditorServices;
 }

+ 323 - 2
src/notebook/cells/editor.ts

@@ -6,7 +6,7 @@ import {
 } from 'phosphor/lib/algorithm/json';
 
 import {
-  ISignal
+  defineSignal, ISignal
 } from 'phosphor/lib/core/signaling';
 
 import {
@@ -17,6 +17,18 @@ import {
   ICellModel,
 } from './model';
 
+import {
+  CodeEditor
+} from '../../codeeditor';
+
+import {
+  CodeEditorWidget
+} from '../../codeeditor/widget';
+
+import {
+  IChangedArgs
+} from '../../common/interfaces';
+
 
 /**
  * The location of requested edges.
@@ -125,7 +137,7 @@ interface ICellEditorWidget extends Widget {
   /**
    * The cell model used by the editor.
    */
-  model: ICellModel;
+  model: ICellModel | null;
 
   /**
    * A signal emitted when either the top or bottom edge is requested.
@@ -188,3 +200,312 @@ interface ICellEditorWidget extends Widget {
    */
   setCursor(line: number, character: number): void;
 }
+
+/**
+ * The key code for the up arrow key.
+ */
+const UP_ARROW = 38;
+
+/**
+ * The key code for the down arrow key.
+ */
+const DOWN_ARROW = 40;
+
+/**
+ * The key code for the tab key.
+ */
+const TAB = 9;
+
+/**
+ * The class name added to cell editor widget nodes.
+ */
+const CELL_EDITOR_CLASS = 'jp-CellEditor';
+
+/**
+ * The class name added to read only cell editor widgets.
+ */
+const READ_ONLY_CLASS = 'jp-mod-readOnly';
+
+
+/**
+ * A code editor widget for a cell editor.
+ */
+export
+class CodeCellEditorWidget extends CodeEditorWidget implements ICellEditorWidget {
+  /**
+   * Construct a new cell editor widget.
+   */
+  constructor(editorFactory: (host: Widget) => CodeEditor.IEditor) {
+    super(editorFactory);
+    this.addClass(CELL_EDITOR_CLASS);
+
+    this.editor.model.valueChanged.connect((editorModel, valueChange) => {
+      this.onEditorModelChange(this.editor, editorModel, valueChange);
+    });
+    this.editor.onKeyDown = (editor, event) => {
+      return this.onEditorKeydown(editor, event);
+    };
+  }
+
+  /**
+   * A signal emitted when a tab (text) completion is requested.
+   */
+  completionRequested: ISignal<ICellEditorWidget, ICompletionRequest>;
+
+  /**
+   * A signal emitted when either the top or bottom edge is requested.
+   */
+  edgeRequested: ISignal<ICellEditorWidget, EdgeLocation>;
+
+  /**
+   * A signal emitted when a text change is completed.
+   */
+  textChanged: ISignal<ICellEditorWidget, ITextChange>;
+
+  /**
+   * The cell model used by the editor.
+   */
+  get model(): ICellModel | null {
+    return this._model;
+  }
+  set model(model: ICellModel | null) {
+    if (!model && !this._model || model === this._model) {
+      return;
+    }
+
+    const oldValue = this._model;
+    this._model = model;
+    this.onModelChanged(oldValue, model);
+  }
+
+  /**
+   * The line numbers state of the editor.
+   */
+  get lineNumbers(): boolean {
+    return this.editor.lineNumbers;
+  }
+  set lineNumbers(value: boolean) {
+    this.editor.lineNumbers = value;
+  }
+
+  /**
+   * Dispose of the resources held by the editor.
+   */
+  dispose(): void {
+    this._model = null;
+    super.dispose();
+  }
+
+  /**
+   * Change the mode for an editor based on the given mime type.
+   */
+  setMimeType(mimeType: string): void {
+    this.editor.model.mimeType = mimeType;
+  }
+
+  /**
+   * Set whether the editor is read only.
+   */
+  setReadOnly(readOnly: boolean): void {
+    let option = readOnly ? true : false;
+    this.editor.readOnly = option;
+    this.toggleClass(READ_ONLY_CLASS, option);
+  }
+
+  /**
+   * Test whether the editor has keyboard focus.
+   */
+  hasFocus(): boolean {
+    return this.editor.hasFocus();
+  }
+
+  /**
+   * Returns a zero-based last line number.
+   */
+  getLastLine(): number {
+    let editorModel = this.editor.model;
+    return editorModel.lineCount - 1;
+  }
+
+  /**
+   * Get the current cursor position of the editor.
+   */
+  getCursorPosition(): number {
+    const cursorPosition = this.editor.getCursorPosition();
+    return this.editor.model.getOffsetAt(cursorPosition);
+  }
+
+  /**
+   * Set the position of the cursor.
+   *
+   * @param position - A new cursor's position.
+   */
+  setCursorPosition(offset: number): void {
+    const position = this.editor.model.getPositionAt(offset);
+    this.editor.setCursorPosition(position);
+  }
+
+  /**
+   * Set the position of the cursor.
+   *
+   * @param line - A zero-based line number.
+   *
+   * @param character - A zero-based character number.
+   */
+  setCursor(line: number, character: number): void {
+    let editorModel = this.editor.model;
+    let position = editorModel.getOffsetAt({
+      line: line,
+      column: character
+    });
+    this.setCursorPosition(position);
+  }
+
+  /**
+   * Handle changes in the model.
+   *
+   * #### Notes
+   * Subclasses may override this method as needed.
+   */
+  protected onModelChanged(oldValue: ICellModel | null, newValue: ICellModel | null): void {
+    // If the model is being replaced, disconnect the old signal handler.
+    if (oldValue) {
+      oldValue.stateChanged.disconnect(this.onModelStateChanged, this);
+    }
+
+    if (newValue) {
+      this.editor.model.value = newValue.source || '';
+      this.editor.model.clearHistory();
+      newValue.stateChanged.connect(this.onModelStateChanged, this);
+    } else {
+      this.editor.model.value = '';
+    }
+  }
+
+  /**
+   * Handle changes in the model state.
+   */
+  protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
+    switch (args.name) {
+    case 'source':
+      let editorModel = this.editor.model;
+      if (editorModel.value !== args.newValue) {
+        editorModel.value = args.newValue;
+      }
+      break;
+    default:
+      break;
+    }
+  }
+
+  /**
+   * Handle change events from the editor model.
+   */
+  protected onEditorModelChange(editor: CodeEditor.IEditor, editorModel: CodeEditor.IModel, valueChange: IChangedArgs<string>): void {
+    let model = this.model;
+    let oldValue = valueChange.oldValue;
+    let newValue = valueChange.newValue;
+    let cursorPosition = editor.getCursorPosition();
+    let position = editorModel.getOffsetAt(cursorPosition);
+    let line = cursorPosition.line;
+    let ch = cursorPosition.column;
+    let coords = editor.getCoordinate(cursorPosition) as ICoords;
+    let chHeight = editor.lineHeight;
+    let chWidth = editor.charWidth;
+    if (model) {
+      model.source = newValue;
+    }
+    this.textChanged.emit({
+      line, ch, chHeight, chWidth, coords, position, oldValue, newValue
+    });
+  }
+
+  /**
+   * Handle keydown events from the editor.
+   */
+  protected onEditorKeydown(editor: CodeEditor.IEditor, event: KeyboardEvent): boolean {
+    let editorModel = editor.model;
+    let cursorPosition = editor.getCursorPosition();
+    let line = cursorPosition.line;
+    let ch = cursorPosition.column;
+
+    if (event.keyCode === TAB) {
+      // If the tab is modified, ignore it.
+      if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
+        return false;
+      }
+      this.onTabEvent(event, ch, line);
+      return false;
+    }
+
+    if (line === 0 && ch === 0 && event.keyCode === UP_ARROW) {
+        if (!event.shiftKey) {
+          this.edgeRequested.emit('top');
+        }
+        return false;
+    }
+
+    let lastLine = editorModel.lineCount - 1;
+    let lastCh = editorModel.getLine(editorModel.lineCount - 1).length;
+    if (line === lastLine && ch === lastCh && event.keyCode === DOWN_ARROW) {
+      if (!event.shiftKey) {
+        this.edgeRequested.emit('bottom');
+      }
+      return false;
+    }
+    return false;
+  }
+
+  /**
+   * Handle a tab key press.
+   */
+  protected onTabEvent(event: KeyboardEvent, ch: number, line: number): void {
+    let editor = this.editor;
+    let editorModel = editor.model;
+
+    // If there is a text selection, no completion requests should be emitted.
+    const selection = editor.getSelection();
+    if (selection.start === selection.end) {
+      return;
+    }
+
+    let currentValue = editorModel.value;
+    let currentLine = currentValue.split('\n')[line];
+    let chHeight = editor.lineHeight;
+    let chWidth = editor.charWidth;
+    let coords = editor.getCoordinate({
+      line: line,
+      column: ch
+    });
+    let position = editorModel.getOffsetAt({
+      line: line,
+      column: ch
+    });
+
+    // A completion request signal should only be emitted if the current
+    // character or a preceding character is not whitespace.
+    //
+    // Otherwise, the default tab action of creating a tab character should be
+    // allowed to propagate.
+    if (!currentLine.substring(0, ch).match(/\S/)) {
+      return;
+    }
+
+    event.preventDefault();
+    event.stopPropagation();
+    event.stopImmediatePropagation();
+
+    let data = {
+      line, ch, chHeight, chWidth, coords, position, currentValue
+    };
+    this.completionRequested.emit(data as ICompletionRequest);
+  }
+
+  private _model: ICellModel | null = null;
+}
+
+
+// Define the signals for the `CodeCellEditorWidget` class.
+defineSignal(CodeCellEditorWidget.prototype, 'completionRequested');
+defineSignal(CodeCellEditorWidget.prototype, 'edgeRequested');
+defineSignal(CodeCellEditorWidget.prototype, 'textChanged');

+ 1 - 0
src/notebook/cells/index.ts

@@ -1,5 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+export * from './editor';
 export * from './model';
 export * from './widget';

+ 40 - 4
src/notebook/cells/widget.ts

@@ -25,6 +25,10 @@ import {
   IChangedArgs
 } from '../../common/interfaces';
 
+import {
+  CodeEditor
+} from '../../codeeditor';
+
 import {
   RenderMime
 } from '../../rendermime';
@@ -38,7 +42,7 @@ import {
 } from '../output-area';
 
 import {
-  ICellEditorWidget
+  ICellEditorWidget, CodeCellEditorWidget
 } from './editor';
 
 import {
@@ -389,11 +393,26 @@ namespace BaseCellWidget {
    * The default implementation of an `IRenderer`.
    */
   export
-  abstract class Renderer implements IRenderer {
+  class Renderer implements IRenderer {
+
+    /**
+     * A code editor factory.
+     */
+    readonly editorFactory: (host: Widget) => CodeEditor.IEditor;
+
+    /**
+     * Creates a new renderer.
+     */
+    constructor(options: Renderer.IOptions) {
+      this.editorFactory = options.editorFactory;
+    }
+
     /**
      * Create a new cell editor for the widget.
      */
-    abstract createCellEditor(): ICellEditorWidget;
+    createCellEditor(): ICellEditorWidget {
+      return new CodeCellEditorWidget(this.editorFactory);
+    }
 
     /**
      * Create a new input area for the widget.
@@ -402,6 +421,23 @@ namespace BaseCellWidget {
       return new InputAreaWidget(editor);
     }
   }
+
+  /**
+   * The namespace for the `Renderer` class statics.
+   */
+  export
+  namespace Renderer {
+    /**
+     * An options object for initializing a renderer.
+     */
+    export
+    interface IOptions {
+      /**
+       * A code editor factory.
+       */
+      readonly editorFactory: (host: Widget) => CodeEditor.IEditor;
+    }
+  }
 }
 
 
@@ -574,7 +610,7 @@ namespace CodeCellWidget {
    * The default implementation of an `IRenderer`.
    */
   export
-  abstract class Renderer extends BaseCellWidget.Renderer implements IRenderer {
+  class Renderer extends BaseCellWidget.Renderer implements IRenderer {
     /**
      * Create an output area widget.
      */

+ 0 - 322
src/notebook/codemirror/cells/editor.ts

@@ -1,322 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import{
-  defineSignal, ISignal
-} from 'phosphor/lib/core/signaling';
-
-import * as CodeMirror
-  from 'codemirror';
-
-import {
-  loadModeByMIME
-} from '../../../codemirror';
-
-import {
-  CodeMirrorWidget
-} from '../../../codemirror/widget';
-
-import {
-  IChangedArgs
-} from '../../../common/interfaces';
-
-import {
-  ICellModel
-} from '../../cells/model';
-
-import {
-  ICellEditorWidget, EdgeLocation, ITextChange, ICompletionRequest, ICoords
-} from '../../cells/editor';
-
-
-/**
- * The key code for the up arrow key.
- */
-const UP_ARROW = 38;
-
-/**
- * The key code for the down arrow key.
- */
-const DOWN_ARROW = 40;
-
-/**
- * The key code for the tab key.
- */
-const TAB = 9;
-
-/**
- * The class name added to cell editor widget nodes.
- */
-const CELL_EDITOR_CLASS = 'jp-CellEditor';
-
-/**
- * The class name added to read only cell editor widgets.
- */
-const READ_ONLY_CLASS = 'jp-mod-readOnly';
-
-
-/**
- * A code mirror widget for a cell editor.
- */
-export
-class CodeMirrorCellEditorWidget extends CodeMirrorWidget implements ICellEditorWidget {
-  /**
-   * Construct a new cell editor widget.
-   */
-  constructor(options: CodeMirror.EditorConfiguration = {}) {
-    super(options);
-    this.addClass(CELL_EDITOR_CLASS);
-
-    CodeMirror.on(this.editor.getDoc(), 'change', (instance, change) => {
-      this.onDocChange(instance, change);
-    });
-    CodeMirror.on(this.editor, 'keydown', (instance, evt) => {
-      this.onEditorKeydown(instance, evt);
-    });
-  }
-
-  /**
-   * A signal emitted when a tab (text) completion is requested.
-   */
-  completionRequested: ISignal<ICellEditorWidget, ICompletionRequest>;
-
-  /**
-   * A signal emitted when either the top or bottom edge is requested.
-   */
-  edgeRequested: ISignal<ICellEditorWidget, EdgeLocation>;
-
-  /**
-   * A signal emitted when a text change is completed.
-   */
-  textChanged: ISignal<ICellEditorWidget, ITextChange>;
-
-  /**
-   * The cell model used by the editor.
-   */
-  get model(): ICellModel {
-    return this._model;
-  }
-  set model(model: ICellModel) {
-    if (!model && !this._model || model === this._model) {
-      return;
-    }
-
-    let doc = this.editor.getDoc();
-
-    // If the model is being replaced, disconnect the old signal handler.
-    if (this._model) {
-      this._model.stateChanged.disconnect(this.onModelStateChanged, this);
-    }
-
-    if (!model) {
-      doc.setValue('');
-      this._model = null;
-      return;
-    }
-
-    this._model = model;
-    doc.setValue(this._model.source || '');
-    doc.clearHistory();
-    this._model.stateChanged.connect(this.onModelStateChanged, this);
-  }
-
-  /**
-   * The line numbers state of the editor.
-   */
-  get lineNumbers(): boolean {
-    return this.editor.getOption('lineNumbers');
-  }
-  set lineNumbers(value: boolean) {
-    this.editor.setOption('lineNumbers', value);
-  }
-
-  /**
-   * Dispose of the resources held by the editor.
-   */
-  dispose(): void {
-    this._model = null;
-    super.dispose();
-  }
-
-  /**
-   * Change the mode for an editor based on the given mime type.
-   */
-  setMimeType(mimeType: string): void {
-    loadModeByMIME(this.editor, mimeType);
-  }
-
-  /**
-   * Set whether the editor is read only.
-   */
-  setReadOnly(readOnly: boolean): void {
-    let option = readOnly ? true : false;
-    this.editor.setOption('readOnly', option);
-    this.toggleClass(READ_ONLY_CLASS, option);
-  }
-
-  /**
-   * Test whether the editor has keyboard focus.
-   */
-  hasFocus(): boolean {
-    return this.editor.hasFocus();
-  }
-
-  /**
-   * Returns a zero-based last line number.
-   */
-  getLastLine(): number {
-    return this.editor.getDoc().lastLine();
-  }
-
-  /**
-   * Get the current cursor position of the editor.
-   */
-  getCursorPosition(): number {
-    let doc = this.editor.getDoc();
-    let position = doc.getCursor();
-    return doc.indexFromPos(position);
-  }
-
-  /**
-   * Set the position of the cursor.
-   *
-   * @param position - A new cursor's position.
-   */
-  setCursorPosition(position: number): void {
-    let doc = this.editor.getDoc();
-    doc.setCursor(doc.posFromIndex(position));
-  }
-
-  /**
-   * Set the position of the cursor.
-   *
-   * @param line - A zero-based line number.
-   *
-   * @param character - A zero-based character number.
-   */
-  setCursor(line: number, character: number): void {
-    let doc = this.editor.getDoc();
-    doc.setCursor({
-      line: line,
-      ch: character
-    });
-  }
-
-  /**
-   * Handle changes in the model state.
-   */
-  protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
-    switch (args.name) {
-    case 'source':
-      let doc = this.editor.getDoc();
-      if (doc.getValue() !== args.newValue) {
-        doc.setValue(args.newValue);
-      }
-      break;
-    default:
-      break;
-    }
-  }
-
-  /**
-   * Handle change events from the document.
-   */
-  protected onDocChange(doc: CodeMirror.Doc, change: CodeMirror.EditorChange): void {
-    let model = this.model;
-    let editor = this.editor;
-    let oldValue = model.source;
-    let newValue = doc.getValue();
-    let cursor = doc.getCursor();
-    let line = cursor.line;
-    let ch = cursor.ch;
-    let chHeight = editor.defaultTextHeight();
-    let chWidth = editor.defaultCharWidth();
-    let coords = editor.charCoords({ line, ch }, 'page') as ICoords;
-    let position = editor.getDoc().indexFromPos({ line, ch });
-
-    model.source = newValue;
-    this.textChanged.emit({
-      line, ch, chHeight, chWidth, coords, position, oldValue, newValue
-    });
-  }
-
-  /**
-   * Handle keydown events from the editor.
-   */
-  protected onEditorKeydown(editor: CodeMirror.Editor, event: KeyboardEvent): void {
-    let doc = editor.getDoc();
-    let cursor = doc.getCursor();
-    let line = cursor.line;
-    let ch = cursor.ch;
-
-    if (event.keyCode === TAB) {
-      // If the tab is modified, ignore it.
-      if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
-        return;
-      }
-      return this.onTabEvent(event, ch, line);
-    }
-
-    if (line === 0 && ch === 0 && event.keyCode === UP_ARROW) {
-        if (!event.shiftKey) {
-          this.edgeRequested.emit('top');
-        }
-        return;
-    }
-
-    let lastLine = doc.lastLine();
-    let lastCh = doc.getLineHandle(lastLine).text.length;
-    if (line === lastLine && ch === lastCh && event.keyCode === DOWN_ARROW) {
-      if (!event.shiftKey) {
-        this.edgeRequested.emit('bottom');
-      }
-      return;
-    }
-  }
-
-  /**
-   * Handle a tab key press.
-   */
-  protected onTabEvent(event: KeyboardEvent, ch: number, line: number): void {
-    let editor = this.editor;
-    let doc = editor.getDoc();
-
-    // If there is a text selection, no completion requests should be emitted.
-    if (doc.getSelection()) {
-      return;
-    }
-
-    let currentValue = doc.getValue();
-    let currentLine = currentValue.split('\n')[line];
-    let chHeight = editor.defaultTextHeight();
-    let chWidth = editor.defaultCharWidth();
-    let coords = editor.charCoords({ line, ch }, 'page') as ICoords;
-    let position = editor.getDoc().indexFromPos({ line, ch });
-
-    // A completion request signal should only be emitted if the current
-    // character or a preceding character is not whitespace.
-    //
-    // Otherwise, the default tab action of creating a tab character should be
-    // allowed to propagate.
-    if (!currentLine.substring(0, ch).match(/\S/)) {
-      return;
-    }
-
-    event.preventDefault();
-    event.stopPropagation();
-    event.stopImmediatePropagation();
-
-    let data = {
-      line, ch, chHeight, chWidth, coords, position, currentValue
-    };
-    this.completionRequested.emit(data as ICompletionRequest);
-  }
-
-  private _model: ICellModel = null;
-}
-
-
-// Define the signals for the `CodeMirrorCellEditorWidget` class.
-defineSignal(CodeMirrorCellEditorWidget.prototype, 'completionRequested');
-defineSignal(CodeMirrorCellEditorWidget.prototype, 'edgeRequested');
-defineSignal(CodeMirrorCellEditorWidget.prototype, 'textChanged');

+ 0 - 97
src/notebook/codemirror/cells/widget.ts

@@ -1,97 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  ICellEditorWidget
-} from '../../cells/editor';
-
-import {
-  CodeCellWidget
-} from '../../cells/widget';
-
-import {
-  CodeMirrorCellEditorWidget
-} from './editor';
-
-
-/**
- * A code mirror renderer for a code cell widget.
- */
-export
-class CodeMirrorCodeCellWidgetRenderer extends CodeCellWidget.Renderer {
-  /**
-   * Construct a code mirror renderer for a code cell widget.
-   * @param editorConfiguration a code mirror editor configuration
-   * @param editorInitializer a code cell widget initializer
-   */
-  constructor(options: CodeMirrorCodeCellWidgetRenderer.IOptions = {}) {
-    super();
-    this._editorConfiguration = (options.editorConfiguration ||
-      CodeMirrorCodeCellWidgetRenderer.defaultEditorConfiguration);
-    this._editorInitializer = (options.editorInitializer ||
-      (editor => { /* no-op */ }));
-  }
-
-  /**
-   * Construct a code cell widget.
-   */
-  createCellEditor(): ICellEditorWidget {
-    const widget = new CodeMirrorCellEditorWidget(this._editorConfiguration);
-    this._editorInitializer(widget);
-    return widget;
-  }
-
-  private _editorConfiguration: CodeMirror.EditorConfiguration = null;
-  private _editorInitializer: (editor: CodeMirrorCellEditorWidget) => void = null;
-}
-
-
-/**
- * A namespace for `CodeMirrorCodeCellWidgetRenderer` statics.
- */
-export
-namespace CodeMirrorCodeCellWidgetRenderer {
-  /**
-   * The options used to construct a code mirror code cell widget renderer.
-   */
-  export
-  interface IOptions {
-    /**
-     * A code mirror editor configuration.
-     */
-    editorConfiguration?: CodeMirror.EditorConfiguration;
-
-    /**
-     * A code cell widget initializer function.
-     */
-    editorInitializer?: (editor: CodeMirrorCellEditorWidget) => void;
-  }
-
-  /**
-   * A default code mirror configuration for a cell editor.
-   */
-  export
-  const defaultEditorConfiguration: CodeMirror.EditorConfiguration = {
-    // Default value of the theme is set in the parent constructor,
-    // but could be overridden here
-    indentUnit: 4,
-    readOnly: false,
-    extraKeys: {
-      'Cmd-Right': 'goLineRight',
-      'End': 'goLineRight',
-      'Cmd-Left': 'goLineLeft',
-      'Tab': 'indentMore',
-      'Shift-Tab': 'indentLess',
-      'Cmd-Alt-[': 'indentAuto',
-      'Ctrl-Alt-[': 'indentAuto',
-      'Cmd-/': 'toggleComment',
-      'Ctrl-/': 'toggleComment',
-    }
-  };
-
-  /**
-   * A default code mirror renderer for a code cell widget.
-   */
-  export
-  const defaultRenderer = new CodeMirrorCodeCellWidgetRenderer();
-}

+ 95 - 0
src/notebook/codemirror/index.ts

@@ -0,0 +1,95 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  NotebookPanel, Notebook
+} from '../notebook';
+
+import {
+  BaseCellWidget, CodeCellWidget, CodeCellEditorWidget
+} from '../cells';
+
+import {
+  IEditorServices, IEditorFactory
+} from '../../codeeditor';
+
+import {
+  CodeMirrorEditorFactory, CodeMirrorMimeTypeService
+} from '../../codemirror';
+
+
+/**
+ * Create a base cell renderer.
+ */
+export
+function createBaseCellRenderer(factory?: IEditorFactory): BaseCellWidget.Renderer {
+  factory = factory || new CodeMirrorEditorFactory();
+  return new BaseCellWidget.Renderer({
+    editorFactory: host => factory.newInlineEditor(host.node, {
+      wordWrap: true
+    })
+  });
+};
+
+
+/**
+ * Create a new code cell renderer.
+ */
+export
+function createCodeCellRenderer(factory?: IEditorFactory): CodeCellWidget.Renderer {
+  factory = factory || new CodeMirrorEditorFactory();
+  return new CodeCellWidget.Renderer({
+    editorFactory: host => factory.newInlineEditor(host.node, {
+      extra: {
+        matchBrackets: true,
+        autoCloseBrackets: true
+      }
+    })
+  });
+}
+
+
+/**
+ * Create a cell editor widget given a factory.
+ */
+export
+function createCellEditor(factory?: IEditorFactory): CodeCellEditorWidget {
+  factory = factory || new CodeMirrorEditorFactory();
+  return new CodeCellEditorWidget(
+    host => factory.newInlineEditor(host.node, {
+      wordWrap: true
+    })
+  );
+}
+
+
+/**
+ * Create a notebook renderer.
+ */
+export
+function createNotebookRenderer(editorServices?: IEditorServices): Notebook.Renderer {
+  editorServices = editorServices || {
+    factory: new CodeMirrorEditorFactory(),
+    mimeTypeService: new CodeMirrorMimeTypeService()
+  };
+  let factory = editorServices.factory;
+  const editorMimeTypeService = editorServices.mimeTypeService;
+  const codeCellRenderer = createCodeCellRenderer(factory);
+  const rawCellRenderer = createBaseCellRenderer(factory);
+  const markdownCellRenderer = rawCellRenderer;
+  return new Notebook.Renderer({
+    codeCellRenderer, markdownCellRenderer, rawCellRenderer, editorMimeTypeService
+  });
+}
+
+
+/**
+ * Create a notebook panel renderer.
+ */
+export
+function createNotebookPanelRenderer(editorServices?: IEditorServices): NotebookPanel.IRenderer {
+  const notebookRenderer = createNotebookRenderer(editorServices);
+  return new NotebookPanel.Renderer({
+    notebookRenderer
+  });
+}

+ 0 - 47
src/notebook/codemirror/notebook/panel.ts

@@ -1,47 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  RenderMime
-} from '../../../rendermime';
-
-import {
-  Notebook
-} from '../../notebook/widget';
-
-import {
-  NotebookPanel
-} from '../../notebook/panel';
-
-import {
-  CodeMirrorNotebookRenderer
-} from './widget';
-
-
-/**
- * A code mirror renderer for a notebook panel.
- */
-export
-class CodeMirrorNotebookPanelRenderer extends NotebookPanel.Renderer {
-  /**
-   * Create a notebook.
-   */
-  createContent(rendermime: RenderMime): Notebook {
-    return new Notebook({
-      rendermime,
-      renderer: CodeMirrorNotebookRenderer.defaultRenderer
-    });
-  }
-}
-
-
-/**
- * A namespace for `CodeMirrorNotebookPanelRenderer` statics.
- */
-export namespace CodeMirrorNotebookPanelRenderer {
-  /**
-   * A default code mirror renderer for a notebook panel.
-   */
-  export
-  const defaultRenderer = new CodeMirrorNotebookPanelRenderer();
-}

+ 0 - 125
src/notebook/codemirror/notebook/widget.ts

@@ -1,125 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  nbformat
-} from '@jupyterlab/services';
-
-import {
-  RenderMime
-} from '../../../rendermime';
-
-import {
-  ICodeCellModel, IMarkdownCellModel, IRawCellModel
-} from '../../cells/model';
-
-import {
-  CodeCellWidget, MarkdownCellWidget, RawCellWidget
-} from '../../cells/widget';
-
-import {
-  mimetypeForLanguage
-  } from '../../common/mimetype';
-
-import {
-  Notebook
-} from '../../notebook/widget';
-
-import {
-  CodeMirrorCodeCellWidgetRenderer
-} from '../cells/widget';
-
-
-/**
- * A code mirror renderer for a notebook.
- */
-export
-class CodeMirrorNotebookRenderer extends Notebook.Renderer {
-  /**
-   * Create a code cell editor.
-   */
-  createCodeCell(model: ICodeCellModel, rendermime: RenderMime): CodeCellWidget {
-    const widget = new CodeCellWidget({
-      rendermime,
-      renderer: CodeMirrorNotebookRenderer.defaultCodeCellRenderer
-    });
-    widget.model = model;
-    return widget;
-  }
-
-  /**
-   * Create a markdown cell editor.
-   */
-  createMarkdownCell(model: IMarkdownCellModel, rendermime: RenderMime): MarkdownCellWidget {
-    const widget = new MarkdownCellWidget({
-      rendermime,
-      renderer: CodeMirrorNotebookRenderer.defaultMarkdownCellRenderer
-    });
-    widget.model = model;
-    return widget;
-  }
-
-  /**
-   * Create a raw cell editor.
-   */
-  createRawCell(model: IRawCellModel): RawCellWidget {
-    const widget = new RawCellWidget({
-      renderer: CodeMirrorNotebookRenderer.defaultRawCellRenderer
-    });
-    widget.model = model;
-    return widget;
-  }
-
-  /**
-   * Get the preferred mimetype given language info.
-   */
-  getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string {
-    return mimetypeForLanguage(info);
-  }
-}
-
-
-/**
- * A namespace for `CodeMirrorNotebookRenderer` statics.
- */
-export
-namespace CodeMirrorNotebookRenderer {
-  /**
-   * A default code mirror renderer for a code cell editor.
-   */
-  export
-  const defaultCodeCellRenderer = new CodeMirrorCodeCellWidgetRenderer({
-    editorInitializer: (editor) => {
-      editor.editor.setOption('matchBrackets', true);
-      editor.editor.setOption('autoCloseBrackets', true);
-    }
-  });
-
-  /**
-   * A default code mirror renderer for a markdown cell editor.
-   */
-  export
-  const defaultMarkdownCellRenderer = new CodeMirrorCodeCellWidgetRenderer({
-    editorInitializer: (editor) => {
-      // Turn on line wrapping for markdown cells.
-      editor.editor.setOption('lineWrapping', true);
-    }
-  });
-
-  /**
-   * A default code mirror renderer for a raw cell editor.
-   */
-  export
-  const defaultRawCellRenderer = new CodeMirrorCodeCellWidgetRenderer({
-    editorInitializer: (editor) => {
-      // Turn on line wrapping for markdown cells.
-      editor.editor.setOption('lineWrapping', true);
-    }
-  });
-
-  /**
-   * A default code mirror renderer for a notebook.
-   */
-  export
-  const defaultRenderer = new CodeMirrorNotebookRenderer();
-}

+ 20 - 6
src/notebook/codemirror/plugin.ts

@@ -3,15 +3,20 @@
 
 import {
   NotebookPanel
-} from '../notebook/panel';
+} from '../notebook';
 
 import {
-  CodeMirrorNotebookPanelRenderer
-} from './notebook/panel';
+  JupyterLab, JupyterLabPlugin
+} from '../../application';
 
 import {
-  JupyterLabPlugin
-} from '../../application';
+  IEditorServices
+} from '../../codeeditor';
+
+import {
+  createNotebookPanelRenderer
+} from '.';
+
 
 /**
  * The provider for a notebook's code mirror renderer.
@@ -19,6 +24,15 @@ import {
 export
 const plugin: JupyterLabPlugin<NotebookPanel.IRenderer> = {
   id: 'jupyter.services.notebook.codemirror.renderer',
+  requires: [IEditorServices],
   provides: NotebookPanel.IRenderer,
-  activate: () => CodeMirrorNotebookPanelRenderer.defaultRenderer
+  activate: activateRendererProvider
 };
+
+/**
+ * Activates the renderer provider extension.
+ */
+function activateRendererProvider(app: JupyterLab, editorServices: IEditorServices): NotebookPanel.IRenderer {
+  return createNotebookPanelRenderer(editorServices);
+}
+

+ 0 - 37
src/notebook/common/mimetype.ts

@@ -1,37 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import * as CodeMirror
-  from 'codemirror';
-
-import {
-  nbformat
-} from '@jupyterlab/services';
-
-
-import {
-  findMode
-} from '../../codemirror';
-
-
-/**
- * Get the appropriate codemirror mimetype given language info.
- */
-export
-function mimetypeForLanguage(info: nbformat.ILanguageInfoMetadata): string {
-  if (info.codemirror_mode) {
-    return findMode(info.codemirror_mode as any).mime;
-  }
-  let mode = CodeMirror.findModeByMIME(info.mimetype || '');
-  if (mode) {
-    return info.mimetype;
-  }
-  let ext = info.file_extension || '';
-  ext = ext.split('.').slice(-1)[0];
-  mode = CodeMirror.findModeByExtension(ext || '');
-  if (mode) {
-    return mode.mime;
-  }
-  mode = CodeMirror.findModeByName(info.name || '');
-  return mode ? mode.mime : 'text/plain';
-}

+ 37 - 2
src/notebook/notebook/panel.ts

@@ -419,11 +419,29 @@ export namespace NotebookPanel {
    * The default implementation of an `IRenderer`.
    */
   export
-  abstract class Renderer implements IRenderer {
+  class Renderer implements IRenderer {
+
+    /**
+     * The notebook renderer.
+     */
+    readonly notebookRenderer: Notebook.Renderer;
+
+    /**
+     * Creates a new renderer.
+     */
+    constructor(options: Renderer.IOptions) {
+      this.notebookRenderer = options.notebookRenderer;
+    }
+
     /**
      * Create a new content area for the panel.
      */
-    abstract createContent(rendermime: RenderMime): Notebook;
+    createContent(rendermime: RenderMime): Notebook {
+      return new Notebook({
+        rendermime,
+        renderer: this.notebookRenderer
+      });
+    }
 
     /**
      * Create a new toolbar for the panel.
@@ -440,6 +458,23 @@ export namespace NotebookPanel {
     }
   }
 
+  /**
+   * The namespace for `Renderer`.
+   */
+  export
+  namespace Renderer {
+    /**
+     * An initialization options for a notebook panel renderer.
+     */
+    export
+    interface IOptions {
+      /**
+       * The notebook renderer.
+       */
+      readonly notebookRenderer: Notebook.Renderer;
+    }
+  }
+
   /* tslint:disable */
   /**
    * The notebook renderer token.

+ 95 - 6
src/notebook/notebook/widget.ts

@@ -69,6 +69,10 @@ import {
   RenderMime
 } from '../../rendermime';
 
+import {
+  IEditorMimeTypeService
+} from '../../codeeditor';
+
 import {
   ICellModel, BaseCellWidget, MarkdownCellModel,
   CodeCellWidget, MarkdownCellWidget,
@@ -535,21 +539,72 @@ namespace StaticNotebook {
    * The default implementation of an `IRenderer`.
    */
   export
-  abstract class Renderer implements IRenderer {
+  class Renderer implements IRenderer {
+
+    /**
+     * A code cell renderer.
+     */
+    readonly codeCellRenderer: CodeCellWidget.IRenderer;
+
+    /**
+     * A markdown cell renderer.
+     */
+    readonly markdownCellRenderer: BaseCellWidget.IRenderer;
+
+    /**
+     * A raw cell renderer.
+     */
+    readonly rawCellRenderer: BaseCellWidget.IRenderer;
+
+    /**
+     * A mime type service of a code editor.
+     */
+    readonly editorMimeTypeService: IEditorMimeTypeService;
+
+    /**
+     * Creates a new renderer.
+     */
+    constructor(options: Renderer.IOptions) {
+      this.codeCellRenderer = options.codeCellRenderer;
+      this.markdownCellRenderer = options.markdownCellRenderer;
+      this.rawCellRenderer = options.rawCellRenderer;
+      this.editorMimeTypeService = options.editorMimeTypeService;
+    }
+
     /**
      * Create a new code cell widget.
      */
-    abstract createCodeCell(model: ICodeCellModel, rendermime: RenderMime): CodeCellWidget;
+    createCodeCell(model: ICodeCellModel, rendermime: RenderMime): CodeCellWidget {
+      const widget = new CodeCellWidget({
+        rendermime,
+        renderer: this.codeCellRenderer
+      });
+      widget.model = model;
+      return widget;
+    }
 
     /**
      * Create a new markdown cell widget.
      */
-    abstract createMarkdownCell(model: IMarkdownCellModel, rendermime: RenderMime): MarkdownCellWidget;
+    createMarkdownCell(model: IMarkdownCellModel, rendermime: RenderMime): MarkdownCellWidget {
+      const widget = new MarkdownCellWidget({
+        rendermime,
+        renderer: this.markdownCellRenderer
+      });
+      widget.model = model;
+      return widget;
+    }
 
     /**
      * Create a new raw cell widget.
      */
-    abstract createRawCell(model: IRawCellModel): RawCellWidget;
+    createRawCell(model: IRawCellModel): RawCellWidget {
+      const widget = new RawCellWidget({
+        renderer: this.rawCellRenderer
+      });
+      widget.model = model;
+      return widget;
+    }
 
     /**
      * Update a cell widget.
@@ -564,7 +619,41 @@ namespace StaticNotebook {
     /**
      * Get the preferred mimetype given language info.
      */
-    abstract getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string;
+    getCodeMimetype(info: nbformat.ILanguageInfoMetadata): string {
+      return this.editorMimeTypeService.getMimeTypeByLanguage(info);
+    }
+  }
+
+  /**
+   * The namespace for the `Renderer` class statics.
+   */
+  export
+  namespace Renderer {
+    /**
+     * An options object for initializing a notebook renderer.
+     */
+    export
+    interface IOptions {
+      /**
+       * A code cell renderer.
+       */
+      readonly codeCellRenderer: CodeCellWidget.IRenderer;
+
+      /**
+       * A markdown cell renderer.
+       */
+      readonly markdownCellRenderer: BaseCellWidget.IRenderer;
+
+      /**
+       * A raw cell renderer.
+       */
+      readonly rawCellRenderer: BaseCellWidget.IRenderer;
+
+      /**
+       * A mime type service of a code editor.
+       */
+      readonly editorMimeTypeService: IEditorMimeTypeService;
+    }
   }
 }
 
@@ -1347,7 +1436,7 @@ namespace Notebook {
    * The default implementation of an `IRenderer`.
    */
   export
-  abstract class Renderer extends StaticNotebook.Renderer { }
+  class Renderer extends StaticNotebook.Renderer { }
 
 }
 

+ 1 - 1
src/renderers/widget.ts

@@ -17,7 +17,7 @@ import {
 
 import {
   DEFAULT_CODEMIRROR_THEME
-} from '../codemirror/widget';
+} from '../codemirror/editor';
 
 import * as marked
   from 'marked';

+ 13 - 26
test/src/completer/handler.spec.ts

@@ -19,13 +19,16 @@ import {
   ICompletionRequest, ICellEditorWidget, ITextChange
 } from '../../../lib/notebook/cells/editor';
 
+import {
+  createBaseCellRenderer
+} from '../../../lib/notebook/codemirror';
+
 import {
   CompleterWidget, CellCompleterHandler, CompleterModel
 } from '../../../lib/completer';
 
-import {
-  CodeMirrorCodeCellWidgetRenderer
-} from '../../../lib/notebook/codemirror/cells/widget';
+
+const renderer = createBaseCellRenderer();
 
 
 class TestCompleterModel extends CompleterModel {
@@ -138,9 +141,7 @@ describe('completer/handler', () => {
         let handler = new CellCompleterHandler({
           completer: new CompleterWidget()
         });
-        let cell = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let cell = new BaseCellWidget({ renderer });
         expect(handler.activeCell).to.be(null);
         handler.activeCell = cell;
         expect(handler.activeCell).to.be.a(BaseCellWidget);
@@ -151,12 +152,8 @@ describe('completer/handler', () => {
         let handler = new CellCompleterHandler({
           completer: new CompleterWidget()
         });
-        let one = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
-        let two = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let one = new BaseCellWidget({ renderer });
+        let two = new BaseCellWidget({ renderer });
         expect(handler.activeCell).to.be(null);
         handler.activeCell = one;
         expect(handler.activeCell).to.be.a(BaseCellWidget);
@@ -356,9 +353,7 @@ describe('completer/handler', () => {
           oldValue: 'fo',
           newValue: 'foo'
         };
-        let cell = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let cell = new BaseCellWidget({ renderer });
 
         handler.activeCell = cell;
         expect(handler.methods).to.not.contain('onTextChanged');
@@ -381,9 +376,7 @@ describe('completer/handler', () => {
           oldValue: 'fo',
           newValue: 'foo'
         };
-        let cell = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let cell = new BaseCellWidget({ renderer });
         let model = completer.model as TestCompleterModel;
 
         handler.activeCell = cell;
@@ -409,9 +402,7 @@ describe('completer/handler', () => {
           position: 0,
           currentValue: 'foo'
         };
-        let cell = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let cell = new BaseCellWidget({ renderer });
 
         handler.activeCell = cell;
         expect(handler.methods).to.not.contain('onCompletionRequested');
@@ -433,9 +424,7 @@ describe('completer/handler', () => {
           position: 0,
           currentValue: 'foo'
         };
-        let cell = new BaseCellWidget({
-          renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer
-        });
+        let cell = new BaseCellWidget({ renderer });
 
         handler.kernel = kernel;
         handler.activeCell = cell;
@@ -463,7 +452,6 @@ describe('completer/handler', () => {
         });
         let handler = new TestCompleterHandler({ completer });
         let model = completer.model as TestCompleterModel;
-        let renderer = CodeMirrorCodeCellWidgetRenderer.defaultRenderer;
 
         handler.activeCell = new BaseCellWidget({ renderer });
         expect(model.methods).to.not.contain('createPatch');
@@ -476,7 +464,6 @@ describe('completer/handler', () => {
         let patch = 'foobar';
         let completer = new CompleterWidget({ model });
         let handler = new TestCompleterHandler({ completer });
-        let renderer = CodeMirrorCodeCellWidgetRenderer.defaultRenderer;
         let cell = new BaseCellWidget({ renderer });
         let request: ICompletionRequest = {
           ch: 0,

+ 8 - 4
test/src/console/content.spec.ts

@@ -20,8 +20,8 @@ import {
 } from 'phosphor/lib/ui/widget';
 
 import {
-  CodeMirrorConsoleRenderer
-} from '../../../lib/console/codemirror/widget';
+  createRenderer
+} from '../../../lib/console/codemirror';
 
 import {
   ConsoleContent
@@ -43,6 +43,10 @@ import {
   EdgeLocation, ICellEditorWidget, ITextChange
 } from '../../../lib/notebook/cells/editor';
 
+import {
+  createCodeCellRenderer
+} from '../../../lib/notebook/codemirror';
+
 import {
   defaultRenderMime
 } from '../utils';
@@ -116,7 +120,7 @@ class TestHistory extends ConsoleHistory {
 
 defineSignal(TestHistory.prototype, 'ready');
 
-const renderer = CodeMirrorConsoleRenderer.defaultRenderer;
+const renderer = createRenderer();
 const rendermime = defaultRenderMime();
 
 
@@ -230,7 +234,7 @@ describe('console/content', () => {
     describe('#addCell()', () => {
 
       it('should add a code cell to the content widget', () => {
-        let renderer = CodeMirrorConsoleRenderer.defaultCodeCellRenderer;
+        let renderer = createCodeCellRenderer();
         let cell = new CodeCellWidget({ renderer, rendermime });
         Widget.attach(widget, document.body);
         expect(widget.cells.length).to.be(0);

+ 3 - 3
test/src/console/foreign.spec.ts

@@ -16,8 +16,8 @@ import {
 } from 'phosphor/lib/ui/panel';
 
 import {
-  CodeMirrorConsoleRenderer
-} from '../../../lib/console/codemirror/widget';
+  createCodeCellRenderer
+} from '../../../lib/notebook/codemirror';
 
 import {
   ForeignHandler
@@ -84,7 +84,7 @@ defineSignal(TestHandler.prototype, 'rejected');
 const rendermime = defaultRenderMime();
 const renderer: ForeignHandler.IRenderer = {
   createCell: () => {
-    let renderer = CodeMirrorConsoleRenderer.defaultCodeCellRenderer;
+    let renderer = createCodeCellRenderer();
     let cell = new CodeCellWidget({ rendermime, renderer });
     cell.model = new CodeCellModel();
     return cell;

+ 3 - 3
test/src/console/panel.spec.ts

@@ -16,8 +16,8 @@ import {
 } from 'phosphor/lib/ui/widget';
 
 import {
-  CodeMirrorConsoleRenderer
-} from '../../../lib/console/codemirror/widget';
+  createRenderer
+} from '../../../lib/console/codemirror';
 
 import {
   ConsoleContent
@@ -48,7 +48,7 @@ class TestPanel extends ConsolePanel {
 }
 
 
-const renderer = CodeMirrorConsoleRenderer.defaultRenderer;
+const renderer = createRenderer();
 const rendermime = defaultRenderMime();
 
 

+ 73 - 73
test/src/notebook/cells/editor.spec.ts

@@ -11,7 +11,19 @@ import {
 } from 'simulate-event';
 
 import {
-  CellModel, ICellModel
+  CodeEditor
+} from '../../../../lib/codeeditor';
+
+import {
+  CodeMirrorEditorFactory
+} from '../../../../lib/codemirror';
+
+import {
+  IChangedArgs
+} from '../../../../lib/common/interfaces';
+
+import {
+  CellModel, ICellModel, CodeCellEditorWidget
 } from '../../../../lib/notebook/cells';
 
 import {
@@ -19,8 +31,8 @@ import {
 } from '../../../../lib/notebook/cells/editor';
 
 import {
-  CodeMirrorCellEditorWidget
-} from '../../../../lib/notebook/codemirror/cells/editor';
+  createCellEditor
+} from '../../../../lib/notebook/codemirror';
 
 
 const UP_ARROW = 38;
@@ -29,23 +41,36 @@ const DOWN_ARROW = 40;
 
 const TAB = 9;
 
+const factory = new CodeMirrorEditorFactory();
+
+
+class LogEditorWidget extends CodeCellEditorWidget {
 
-class LogEditorWidget extends CodeMirrorCellEditorWidget {
   methods: string[] = [];
 
-  protected onModelStateChanged(model: ICellModel, args: any): void {
+  constructor() {
+    super(host => factory.newInlineEditor(host.node, {}));
+  }
+
+  protected onModelChanged(oldValue: ICellModel | null, newValue: ICellModel | null): void {
+    super.onModelChanged(oldValue, newValue);
+    this.methods.push('onModelChanged');
+  }
+
+  protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
     super.onModelStateChanged(model, args);
     this.methods.push('onModelStateChanged');
   }
 
-  protected onDocChange(doc: CodeMirror.Doc, change: CodeMirror.EditorChange): void {
-    super.onDocChange(doc, change);
-    this.methods.push('onDocChange');
+  protected onEditorModelChange(editor: CodeEditor.IEditor, editorModel: CodeEditor.IModel, valueChange: IChangedArgs<string>): void {
+    super.onEditorModelChange(editor, editorModel, valueChange);
+    this.methods.push('onEditorModelChange');
   }
 
-  protected onEditorKeydown(editor: CodeMirror.Editor, event: KeyboardEvent): void {
-    super.onEditorKeydown(editor, event);
+  protected onEditorKeydown(editor: CodeEditor.IEditor, event: KeyboardEvent): boolean {
+    let value = super.onEditorKeydown(editor, event);
     this.methods.push('onEditorKeydown');
+    return value;
   }
 
   protected onTabEvent(event: KeyboardEvent, ch: number, line: number): void {
@@ -57,22 +82,13 @@ class LogEditorWidget extends CodeMirrorCellEditorWidget {
 
 describe('notebook/cells/editor', () => {
 
-  describe('CodeMirrorCellEditorWidget', () => {
+  describe('CodeCellEditorWidget', () => {
 
     describe('#constructor()', () => {
 
       it('should create a cell editor widget', () => {
-        let widget = new CodeMirrorCellEditorWidget();
-        expect(widget).to.be.a(CodeMirrorCellEditorWidget);
-      });
-
-      it('should accept editor configuration options', () => {
-        let widget = new CodeMirrorCellEditorWidget({
-          value: 'foo',
-          mode: 'bar'
-        });
-        expect(widget.editor.getOption('value')).to.be('foo');
-        expect(widget.editor.getOption('mode')).to.be('bar');
+        let widget = createCellEditor();
+        expect(widget).to.be.a(CodeCellEditorWidget);
       });
 
     });
@@ -80,14 +96,14 @@ describe('notebook/cells/editor', () => {
     describe('#lineNumbers', () => {
 
       it('should get the line numbers state of the editor', () => {
-        let widget = new CodeMirrorCellEditorWidget(new CellModel());
-        expect(widget.lineNumbers).to.be(widget.editor.getOption('lineNumbers'));
+        let widget = createCellEditor();
+        expect(widget.lineNumbers).to.be(widget.editor.lineNumbers);
       });
 
       it('should set the line numbers state of the editor', () => {
-        let widget = new CodeMirrorCellEditorWidget(new CellModel());
+        let widget = createCellEditor();
         widget.lineNumbers = !widget.lineNumbers;
-        expect(widget.lineNumbers).to.be(widget.editor.getOption('lineNumbers'));
+        expect(widget.lineNumbers).to.be(widget.editor.lineNumbers);
       });
 
     });
@@ -95,24 +111,26 @@ describe('notebook/cells/editor', () => {
     describe('#edgeRequested', () => {
 
       it('should emit a signal when the top edge is requested', () => {
-        let widget = new CodeMirrorCellEditorWidget(new CellModel());
+        let widget = createCellEditor();
         let edge: EdgeLocation = null;
         let event = generate('keydown', { keyCode: UP_ARROW });
-        let listener = (sender: any, args: EdgeLocation) => { edge = args; }
+        let listener = (sender: any, args: EdgeLocation) => { edge = args; };
         widget.edgeRequested.connect(listener);
         expect(edge).to.be(null);
-        widget.editor.triggerOnKeyDown(event);
+        let editor = (widget.editor as any).editor as CodeMirror.Editor;
+        editor.triggerOnKeyDown(event);
         expect(edge).to.be('top');
       });
 
       it('should emit a signal when the bottom edge is requested', () => {
-        let widget = new CodeMirrorCellEditorWidget(new CellModel());
+        let widget = createCellEditor();
         let edge: EdgeLocation = null;
         let event = generate('keydown', { keyCode: DOWN_ARROW });
-        let listener = (sender: any, args: EdgeLocation) => { edge = args; }
+        let listener = (sender: any, args: EdgeLocation) => { edge = args; };
         widget.edgeRequested.connect(listener);
         expect(edge).to.be(null);
-        widget.editor.triggerOnKeyDown(event);
+        let editor = (widget.editor as any).editor as CodeMirror.Editor;
+        editor.triggerOnKeyDown(event);
         expect(edge).to.be('bottom');
       });
 
@@ -121,24 +139,21 @@ describe('notebook/cells/editor', () => {
     describe('#textChanged', () => {
 
       it('should emit a signal when editor text is changed', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
-        let doc = widget.editor.getDoc();
         let want = { oldValue: '', newValue: 'foo' };
-        let fromPos = { line: 0, ch: 0 };
-        let toPos = { line: 0, ch: 0 };
         let change: ITextChange = null;
         let listener = (sender: any, args: ITextChange) => {
           change = args;
         };
         widget.textChanged.connect(listener);
 
-        // CodeMirrorCellEditorWidget suppresses signals when the code mirror
+        // CodeMirror editor suppresses signals when the code mirror
         // instance's content is changed programmatically via the `setValue`
         // method, so for this test, the `replaceRange` method is being used to
         // generate the text change.
         expect(change).to.not.be.ok();
-        doc.replaceRange(want.newValue, fromPos, toPos);
+        widget.editor.model.value = want.newValue;
         expect(change).to.be.ok();
         expect(change.oldValue).to.equal(want.oldValue);
         expect(change.newValue).to.equal(want.newValue);
@@ -149,12 +164,9 @@ describe('notebook/cells/editor', () => {
     describe('#completionRequested', () => {
 
       it('should emit a signal when the user requests a tab completion', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
-        let doc = widget.editor.getDoc();
         let want = { currentValue: 'foo', line: 0, ch: 3 };
-        let fromPos = { line: 0, ch: 0 };
-        let toPos = { line: 0, ch: 0 };
         let request: ICompletionRequest = null;
         let listener = (sender: any, args: ICompletionRequest) => {
           request = args;
@@ -163,8 +175,11 @@ describe('notebook/cells/editor', () => {
         widget.completionRequested.connect(listener);
 
         expect(request).to.not.be.ok();
-        doc.replaceRange(want.currentValue, fromPos, toPos);
-        widget.editor.triggerOnKeyDown(event);
+        widget.editor.model.value = want.currentValue;
+        widget.setCursorPosition(3);
+
+        let editor = (widget.editor as any).editor as CodeMirror.Editor;
+        editor.triggerOnKeyDown(event);
         expect(request).to.be.ok();
         expect(request.currentValue).to.equal(want.currentValue);
         expect(request.ch).to.equal(want.ch);
@@ -177,7 +192,7 @@ describe('notebook/cells/editor', () => {
 
       it('should be settable', () => {
         let model = new CellModel();
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         expect(widget.model).to.be(null);
         widget.model = model;
         expect(widget.model).to.be(model);
@@ -185,19 +200,19 @@ describe('notebook/cells/editor', () => {
 
       it('should be safe to set multiple times', () => {
         let model = new CellModel();
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
         widget.model = model;
         expect(widget.model).to.be(model);
       });
 
       it('should empty the code mirror if set to null', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
         widget.model.source = 'foo';
-        expect(widget.editor.getDoc().getValue()).to.be('foo');
+        expect(widget.editor.model.value).to.be('foo');
         widget.model = null;
-        expect(widget.editor.getDoc().getValue()).to.be.empty();
+        expect(widget.editor.model.value).to.be.empty();
       });
 
     });
@@ -205,7 +220,7 @@ describe('notebook/cells/editor', () => {
     describe('#dispose()', () => {
 
       it('should dispose of the resources held by the widget', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
         expect(widget.model).to.be.ok();
         widget.dispose();
@@ -218,14 +233,12 @@ describe('notebook/cells/editor', () => {
     describe('#getCursorPosition()', () => {
 
       it('should return the cursor position of the editor', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
-        let doc = widget.editor.getDoc();
-        let fromPos = { line: 0, ch: 0 };
-        let toPos = { line: 0, ch: 0 };
 
         expect(widget.getCursorPosition()).to.be(0);
-        doc.replaceRange('foo', fromPos, toPos);
+        widget.model.source = 'foo';
+        widget.setCursorPosition(3);
         expect(widget.getCursorPosition()).to.be(3);
       });
 
@@ -234,7 +247,7 @@ describe('notebook/cells/editor', () => {
     describe('#setCursorPosition()', () => {
 
       it('should set the cursor position of the editor', () => {
-        let widget = new CodeMirrorCellEditorWidget();
+        let widget = createCellEditor();
         widget.model = new CellModel();
         expect(widget.getCursorPosition()).to.be(0);
         widget.model.source = 'foo';
@@ -257,21 +270,6 @@ describe('notebook/cells/editor', () => {
 
     });
 
-    describe('#onDocChange()', () => {
-
-      it('should run when the code mirror document changes', () => {
-        let widget = new LogEditorWidget();
-        widget.model = new CellModel();
-        let doc = widget.editor.getDoc();
-        let fromPos = { line: 0, ch: 0 };
-        let toPos = { line: 0, ch: 0 };
-        expect(widget.methods).to.not.contain('onDocChange');
-        doc.replaceRange('foo', fromPos, toPos);
-        expect(widget.methods).to.contain('onDocChange');
-      });
-
-    });
-
     describe('#onEditorKeydown()', () => {
 
       it('should run when there is a keydown event on the editor', () => {
@@ -279,7 +277,8 @@ describe('notebook/cells/editor', () => {
         widget.model = new CellModel();
         let event = generate('keydown', { keyCode: UP_ARROW });
         expect(widget.methods).to.not.contain('onEditorKeydown');
-        widget.editor.triggerOnKeyDown(event);
+        let editor = (widget.editor as any).editor as CodeMirror.Editor;
+        editor.triggerOnKeyDown(event);
         expect(widget.methods).to.contain('onEditorKeydown');
       });
 
@@ -292,7 +291,8 @@ describe('notebook/cells/editor', () => {
         widget.model = new CellModel();
         let event = generate('keydown', { keyCode: TAB });
         expect(widget.methods).to.not.contain('onTabEvent');
-        widget.editor.triggerOnKeyDown(event);
+        let editor = (widget.editor as any).editor as CodeMirror.Editor;
+        editor.triggerOnKeyDown(event);
         expect(widget.methods).to.contain('onTabEvent');
       });
 

+ 20 - 71
test/src/notebook/cells/widget.spec.ts

@@ -18,36 +18,22 @@ import {
 import {
   BaseCellWidget, CellModel, InputAreaWidget, ICellModel,
   CodeCellWidget, CodeCellModel, MarkdownCellWidget,
-  RawCellWidget
+  RawCellWidget, CodeCellEditorWidget
 } from '../../../../lib/notebook/cells';
 
 import {
-  ICellEditorWidget
-} from '../../../../lib/notebook/cells/editor';
-
-import {
-  CodeMirrorCellEditorWidget
-} from '../../../../lib/notebook/codemirror/cells/editor';
-
-import {
-  CodeMirrorCodeCellWidgetRenderer
-} from '../../../../lib/notebook/codemirror/cells/widget';
+  createBaseCellRenderer, createCodeCellRenderer, createCellEditor
+} from '../../../../lib/notebook/codemirror';
 
 import {
   OutputAreaWidget
 } from '../../../../lib/notebook/output-area';
 
-import {
-  RenderMime
-} from '../../../../lib/rendermime';
-
 import {
   defaultRenderMime
 } from '../../utils';
 
 
-const INPUT_CLASS = 'jp-InputArea';
-
 const RENDERED_CLASS = 'jp-mod-rendered';
 
 const PROMPT_CLASS = 'jp-Cell-prompt';
@@ -60,7 +46,7 @@ class LogBaseCell extends BaseCellWidget {
   methods: string[] = [];
 
   constructor() {
-    super({ renderer: CodeMirrorCodeCellWidgetRenderer.defaultRenderer });
+    super({ renderer: createBaseCellRenderer() });
   }
 
   renderInput(widget: Widget): void {
@@ -142,32 +128,12 @@ class LogMarkdownCell extends MarkdownCellWidget {
 }
 
 
-class LogRenderer extends CodeMirrorCodeCellWidgetRenderer {
-  methods: string[] = [];
-
-  createCellEditor(): ICellEditorWidget {
-    this.methods.push('createCellEditor');
-    return super.createCellEditor();
-  }
-
-  createInputArea(editor: ICellEditorWidget): InputAreaWidget {
-    this.methods.push('createInputArea');
-    return super.createInputArea(editor);
-  }
-
-  createOutputArea(rendermime: RenderMime): OutputAreaWidget {
-    this.methods.push('createOutputArea');
-    return super.createOutputArea(rendermime);
-  }
-}
-
-
 describe('notebook/cells/widget', () => {
 
-  let renderer = new LogRenderer();
-
   describe('BaseCellWidget', () => {
 
+    let renderer = createBaseCellRenderer();
+
     describe('#constructor()', () => {
 
       it('should create a base cell widget', () => {
@@ -176,16 +142,9 @@ describe('notebook/cells/widget', () => {
       });
 
       it('should accept a custom renderer', () => {
-        renderer = new LogRenderer();
-
-        expect(renderer.methods).to.not.contain('createCellEditor');
-        expect(renderer.methods).to.not.contain('createInputArea');
-
+        renderer = createBaseCellRenderer();
         let widget = new BaseCellWidget({ renderer });
-
         expect(widget).to.be.a(BaseCellWidget);
-        expect(renderer.methods).to.contain('createCellEditor');
-        expect(renderer.methods).to.contain('createInputArea');
       });
 
     });
@@ -235,7 +194,7 @@ describe('notebook/cells/widget', () => {
 
       it('should be a cell editor widget', () => {
         let widget = new BaseCellWidget({ renderer });
-        expect(widget.editor).to.be.a(CodeMirrorCellEditorWidget);
+        expect(widget.editor).to.be.a(CodeCellEditorWidget);
       });
 
     });
@@ -486,7 +445,7 @@ describe('notebook/cells/widget', () => {
 
         it('should create a cell editor widget', () => {
           let editor = renderer.createCellEditor();
-          expect(editor).to.be.a(CodeMirrorCellEditorWidget);
+          expect(editor).to.be.a(CodeCellEditorWidget);
         });
 
       });
@@ -515,6 +474,8 @@ describe('notebook/cells/widget', () => {
 
   describe('CodeCellWidget', () => {
 
+    let renderer = createCodeCellRenderer();
+
     describe('#constructor()', () => {
 
       it('should create a code cell widget', () => {
@@ -523,19 +484,10 @@ describe('notebook/cells/widget', () => {
       });
 
       it('should accept a custom renderer', () => {
-        renderer = new LogRenderer();
-
-        expect(renderer.methods).to.not.contain('createCellEditor');
-        expect(renderer.methods).to.not.contain('createInputArea');
-        expect(renderer.methods).to.not.contain('createOutputArea');
-
+        renderer = createCodeCellRenderer();
         let widget = new CodeCellWidget({ renderer, rendermime });
         widget.model = new CodeCellModel();
-
         expect(widget).to.be.a(CodeCellWidget);
-        expect(renderer.methods).to.contain('createCellEditor');
-        expect(renderer.methods).to.contain('createInputArea');
-        expect(renderer.methods).to.contain('createOutputArea');
       });
 
     });
@@ -674,6 +626,8 @@ describe('notebook/cells/widget', () => {
 
   describe('MarkdownCellWidget', () => {
 
+    let renderer = createBaseCellRenderer();
+
     describe('#constructor()', () => {
 
       it('should create a markdown cell widget', () => {
@@ -682,16 +636,9 @@ describe('notebook/cells/widget', () => {
       });
 
       it('should accept a custom renderer', () => {
-        renderer = new LogRenderer();
-
-        expect(renderer.methods).to.not.contain('createCellEditor');
-        expect(renderer.methods).to.not.contain('createInputArea');
-
+        renderer = createBaseCellRenderer();
         let widget = new MarkdownCellWidget({ renderer, rendermime });
-
         expect(widget).to.be.a(MarkdownCellWidget);
-        expect(renderer.methods).to.contain('createCellEditor');
-        expect(renderer.methods).to.contain('createInputArea');
       });
 
       it('should set the default mimetype to text/x-ipythongfm', () => {
@@ -773,6 +720,8 @@ describe('notebook/cells/widget', () => {
 
   describe('RawCellWidget', () => {
 
+    let renderer = createBaseCellRenderer();
+
     describe('#constructor()', () => {
 
       it('should create a raw cell widget', () => {
@@ -789,7 +738,7 @@ describe('notebook/cells/widget', () => {
     describe('#constructor()', () => {
 
       it('should create an input area widget', () => {
-        let editor = new CodeMirrorCellEditorWidget(new CellModel());
+        let editor = createCellEditor();
         let widget = new InputAreaWidget(editor);
         expect(widget).to.be.an(InputAreaWidget);
       });
@@ -799,7 +748,7 @@ describe('notebook/cells/widget', () => {
     describe('#setPrompt()', () => {
 
       it('should change the value of the input prompt', () => {
-        let editor = new CodeMirrorCellEditorWidget(new CellModel());
+        let editor = createCellEditor();
         let widget = new InputAreaWidget(editor);
         let prompt = widget.node.querySelector(`.${PROMPT_CLASS}`);
         expect(prompt.textContent).to.be.empty();
@@ -808,7 +757,7 @@ describe('notebook/cells/widget', () => {
       });
 
       it('should treat the string value "null" as special', () => {
-        let editor = new CodeMirrorCellEditorWidget(new CellModel());
+        let editor = createCellEditor();
         let widget = new InputAreaWidget(editor);
         let prompt = widget.node.querySelector(`.${PROMPT_CLASS}`);
         expect(prompt.textContent).to.be.empty();

+ 5 - 5
test/src/notebook/notebook/actions.spec.ts

@@ -19,6 +19,10 @@ import {
   CodeCellWidget, MarkdownCellWidget, RawCellWidget
 } from '../../../../lib/notebook/cells/widget';
 
+import {
+  createNotebookRenderer
+} from '../../../../lib/notebook/codemirror';
+
 import {
  NotebookModel
 } from '../../../../lib/notebook/notebook/model';
@@ -39,10 +43,6 @@ import {
   DEFAULT_CONTENT
 } from '../utils';
 
-import {
-  CodeMirrorNotebookRenderer
-} from '../../../../lib/notebook/codemirror/notebook/widget';
-
 
 const clipboard = new MimeData();
 const ERROR_INPUT = 'a = foo';
@@ -59,7 +59,7 @@ describe('notebook/notebook/actions', () => {
     beforeEach((done) => {
       widget = new Notebook({
         rendermime: defaultRenderMime(),
-        renderer: CodeMirrorNotebookRenderer.defaultRenderer
+        renderer: createNotebookRenderer()
       });
       let model = new NotebookModel();
       model.fromJSON(DEFAULT_CONTENT);

+ 3 - 3
test/src/notebook/notebook/default-toolbar.spec.ts

@@ -32,8 +32,8 @@ import {
 } from '../../../../lib/notebook/notebook/actions';
 
 import {
-  CodeMirrorNotebookPanelRenderer
-} from '../../../../lib/notebook/codemirror/notebook/panel';
+  createNotebookPanelRenderer
+} from '../../../../lib/notebook/codemirror';
 
 import {
  ToolbarItems
@@ -102,7 +102,7 @@ describe('notebook/notebook/default-toolbar', () => {
   describe('ToolbarItems', () => {
 
     let panel: NotebookPanel;
-    const renderer = CodeMirrorNotebookPanelRenderer.defaultRenderer;
+    const renderer = createNotebookPanelRenderer();
 
     beforeEach(() => {
       panel = new NotebookPanel({ rendermime, clipboard, renderer });

+ 12 - 23
test/src/notebook/notebook/panel.spec.ts

@@ -23,6 +23,10 @@ import {
   CompleterWidget
 } from '../../../../lib/completer';
 
+import {
+  createNotebookPanelRenderer
+} from '../../../../lib/notebook/codemirror';
+
 import {
   INotebookModel
 } from '../../../../lib/notebook/notebook/model';
@@ -47,17 +51,13 @@ import {
   DEFAULT_CONTENT
 } from '../utils';
 
-import {
-  CodeMirrorNotebookPanelRenderer
-} from '../../../../lib/notebook/codemirror/notebook/panel';
-
 
 /**
  * Default data.
  */
 const rendermime = defaultRenderMime();
 const clipboard = new MimeData();
-const renderer = CodeMirrorNotebookPanelRenderer.defaultRenderer;
+const renderer = createNotebookPanelRenderer();
 
 
 class LogNotebookPanel extends NotebookPanel {
@@ -122,7 +122,7 @@ describe('notebook/notebook/panel', () => {
 
 
       it('should accept an optional render', () => {
-        let newRenderer = new CodeMirrorNotebookPanelRenderer();
+        let newRenderer = createNotebookPanelRenderer();
         let panel = new NotebookPanel({
           rendermime, clipboard, renderer: newRenderer
         });
@@ -217,9 +217,9 @@ describe('notebook/notebook/panel', () => {
     describe('#renderer', () => {
 
       it('should be the renderer used by the widget', () => {
-        let renderer = new CodeMirrorNotebookPanelRenderer();
-        let panel = new NotebookPanel({ rendermime, clipboard, renderer });
-        expect(panel.renderer).to.be(renderer);
+        let r = createNotebookPanelRenderer();
+        let panel = new NotebookPanel({ rendermime, clipboard, renderer: r });
+        expect(panel.renderer).to.be(r);
       });
 
     });
@@ -363,8 +363,7 @@ describe('notebook/notebook/panel', () => {
       describe('#createContent()', () => {
 
         it('should create a notebook widget', () => {
-          let r = new CodeMirrorNotebookPanelRenderer();
-          expect(r.createContent(rendermime)).to.be.a(Notebook);
+          expect(renderer.createContent(rendermime)).to.be.a(Notebook);
         });
 
       });
@@ -372,8 +371,7 @@ describe('notebook/notebook/panel', () => {
       describe('#createToolbar()', () => {
 
         it('should create a notebook toolbar', () => {
-          let r = new CodeMirrorNotebookPanelRenderer();
-          expect(r.createToolbar()).to.be.a(Toolbar);
+          expect(renderer.createToolbar()).to.be.a(Toolbar);
         });
 
       });
@@ -381,22 +379,13 @@ describe('notebook/notebook/panel', () => {
       describe('#createCompleter()', () => {
 
         it('should create a completer widget', () => {
-          let r = new CodeMirrorNotebookPanelRenderer();
-          expect(r.createCompleter()).to.be.a(CompleterWidget);
+          expect(renderer.createCompleter()).to.be.a(CompleterWidget);
         });
 
       });
 
     });
 
-    describe('.defaultRenderer', () => {
-
-      it('should be an instance of a `Renderer`', () => {
-        expect(CodeMirrorNotebookPanelRenderer.defaultRenderer).to.be.a(NotebookPanel.Renderer);
-      });
-
-    });
-
   });
 
 });

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

@@ -28,6 +28,10 @@ import {
   RawCellModel, RawCellWidget, BaseCellWidget
 } from '../../../../lib/notebook/cells';
 
+import {
+  createNotebookRenderer
+} from '../../../../lib/notebook/codemirror';
+
 import {
   INotebookModel, NotebookModel
 } from '../../../../lib/notebook/notebook/model';
@@ -44,13 +48,9 @@ import {
   DEFAULT_CONTENT
 } from '../utils';
 
-import {
-  CodeMirrorNotebookRenderer
-} from '../../../../lib/notebook/codemirror/notebook/widget';
-
 
 const rendermime = defaultRenderMime();
-const renderer = CodeMirrorNotebookRenderer.defaultRenderer;
+const renderer = createNotebookRenderer();
 
 
 function createWidget(): LogStaticNotebook {
@@ -170,7 +170,7 @@ describe('notebook/notebook/widget', () => {
       });
 
       it('should accept an optional render', () => {
-        let renderer = new CodeMirrorNotebookRenderer();
+        let renderer = createNotebookRenderer();
         let widget = new StaticNotebook({ rendermime, renderer });
         expect(widget.renderer).to.be(renderer);
       });
@@ -338,7 +338,7 @@ describe('notebook/notebook/widget', () => {
 
       it('should be the cell widget renderer used by the widget', () => {
         let widget = new StaticNotebook({ rendermime, renderer });
-        expect(widget.renderer).to.be(CodeMirrorNotebookRenderer.defaultRenderer);
+        expect(widget.renderer).to.be(renderer);
       });
 
     });
@@ -486,7 +486,7 @@ describe('notebook/notebook/widget', () => {
       describe('#createCodeCell()', () => {
 
         it('should create a `CodeCellWidget`', () => {
-          let renderer = new CodeMirrorNotebookRenderer();
+          let renderer = createNotebookRenderer();
           let model = new CodeCellModel();
           let widget = renderer.createCodeCell(model, rendermime);
           expect(widget).to.be.a(CodeCellWidget);
@@ -497,7 +497,7 @@ describe('notebook/notebook/widget', () => {
       describe('#createMarkdownCell()', () => {
 
         it('should create a `MarkdownCellWidget`', () => {
-          let renderer = new CodeMirrorNotebookRenderer();
+          let renderer = createNotebookRenderer();
           let model = new MarkdownCellModel();
           let widget = renderer.createMarkdownCell(model, rendermime);
           expect(widget).to.be.a(MarkdownCellWidget);
@@ -508,7 +508,7 @@ describe('notebook/notebook/widget', () => {
       describe('#createRawCell()', () => {
 
         it('should create a `RawCellWidget`', () => {
-          let renderer = new CodeMirrorNotebookRenderer();
+          let renderer = createNotebookRenderer();
           let model = new RawCellModel();
           let widget = renderer.createRawCell(model);
           expect(widget).to.be.a(RawCellWidget);
@@ -519,7 +519,7 @@ describe('notebook/notebook/widget', () => {
       describe('#updateCell()', () => {
 
         it('should be a no-op', () => {
-          let renderer = new CodeMirrorNotebookRenderer();
+          let renderer = createNotebookRenderer();
           let model = new CodeCellModel();
           let widget = renderer.createCodeCell(model, rendermime);
           renderer.updateCell(widget);
@@ -531,7 +531,7 @@ describe('notebook/notebook/widget', () => {
       describe('#getCodeMimetype()', () => {
 
         it('should get the preferred mime for code cells in the notebook', () => {
-          let renderer = new CodeMirrorNotebookRenderer();
+          let renderer = createNotebookRenderer();
           let model = new NotebookModel();
           let cursor = model.getMetadata('language_info');
           cursor.setValue({ name: 'python', mimetype: 'text/x-python' });
@@ -543,14 +543,6 @@ describe('notebook/notebook/widget', () => {
 
     });
 
-    describe('.defaultRenderer', () => {
-
-      it('should be an instance of `StaticNotebook.Renderer', () => {
-        expect(CodeMirrorNotebookRenderer.defaultRenderer).to.be.a(StaticNotebook.Renderer);
-      });
-
-    });
-
   });
 
   describe('Notebook', () => {

+ 5 - 6
test/src/notebook/notebook/widgetfactory.spec.ts

@@ -11,6 +11,10 @@ import {
   MimeData
 } from 'phosphor/lib/core/mimedata';
 
+import {
+  createNotebookPanelRenderer
+} from '../../../../lib/notebook/codemirror';
+
 import {
   INotebookModel
 } from '../../../../lib/notebook/notebook/model';
@@ -32,14 +36,9 @@ import {
 } from '../../utils';
 
 
-import {
-  CodeMirrorNotebookPanelRenderer
-} from '../../../../lib/notebook/codemirror/notebook/panel';
-
-
 const rendermime = defaultRenderMime();
 const clipboard = new MimeData();
-const renderer = CodeMirrorNotebookPanelRenderer.defaultRenderer;
+const renderer = createNotebookPanelRenderer();
 
 
 function createFactory(): NotebookWidgetFactory {

+ 5 - 5
test/src/notebook/tracker.spec.ts

@@ -8,12 +8,12 @@ import {
 } from 'phosphor/lib/core/mimedata';
 
 import {
-  BaseCellWidget
-} from '../../../lib/notebook/cells';
+  createNotebookPanelRenderer
+} from '../../../lib/notebook/codemirror';
 
 import {
-  CodeMirrorNotebookPanelRenderer
-} from '../../../lib/notebook/codemirror/notebook/panel';
+  BaseCellWidget
+} from '../../../lib/notebook/cells';
 
 import {
   NotebookPanel
@@ -47,7 +47,7 @@ class TestTracker extends NotebookTracker {
  */
 const rendermime = defaultRenderMime();
 const clipboard = new MimeData();
-const renderer = CodeMirrorNotebookPanelRenderer.defaultRenderer;
+const renderer = createNotebookPanelRenderer();
 
 
 describe('notebook/tracker', () => {

+ 34 - 9
typings/codemirror/codemirror.d.ts

@@ -88,8 +88,8 @@ declare module CodeMirror {
     function off(doc: Doc, eventName: 'cursorActivity', handler: (instance: CodeMirror.Editor) => void ): void;
 
     /** Equivalent to the event by the same name as fired on editor instances. */
-    function on(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: Position; anchor: Position; }) => void ): void;
-    function off(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: Position; anchor: Position; }) => void ): void;
+    function on(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: Selection) => void ): void;
+    function off(doc: Doc, eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: Selection) => void ): void;
 
     /** Will be fired when the line object is deleted. A line object is associated with the start of the line.
     Mostly useful when you need to find out when your gutter markers on a given line are removed. */
@@ -267,11 +267,11 @@ declare module CodeMirror {
 
         /** Scrolls the given element into view. pos is a { line, ch } object, in editor-local coordinates.
         The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */
-        scrollIntoView(pos: { line: number, ch: number }, margin?: number): void;
+        scrollIntoView(pos: Position, margin?: number): void;
 
         /** Scrolls the given element into view. pos is a { from, to } object, in editor-local coordinates.
         The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */
-        scrollIntoView(pos: { from: CodeMirror.Position, to: CodeMirror.Position }, margin: number): void;
+        scrollIntoView(pos: Range, margin?: number): void;
 
         /** Returns an { left , top , bottom } object containing the coordinates of the cursor position.
         If mode is "local" , they will be relative to the top-left corner of the editable document.
@@ -304,7 +304,7 @@ declare module CodeMirror {
         /** Returns a { from , to } object indicating the start (inclusive) and end (exclusive) of the currently rendered part of the document.
         In big documents, when most content is scrolled out of view, CodeMirror will only render the visible part, and a margin around it.
         See also the viewportChange event. */
-        getViewport(): { from: number; to: number };
+        getViewport(): Range;
 
         /** If your code does something to change the size of the editor element (window resizes are already listened for), or unhides it,
         you should probably follow up by calling this method to ensure CodeMirror is still looking as intended. */
@@ -393,8 +393,8 @@ declare module CodeMirror {
 
         /** This event is fired before the selection is moved. Its handler may modify the resulting selection head and anchor.
         Handlers for this event have the same restriction as "beforeChange" handlers � they should not do anything to directly update the state of the editor. */
-        on(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: CodeMirror.Position; anchor: CodeMirror.Position; }) => void ): void;
-        off(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: { head: CodeMirror.Position; anchor: CodeMirror.Position; }) => void ): void;
+        on(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: CodeMirror.Selection) => void ): void;
+        off(eventName: 'beforeSelectionChange', handler: (instance: CodeMirror.Editor, selection: CodeMirror.Selection) => void ): void;
 
         /** Fires whenever the view port of the editor changes (due to scrolling, editing, or any other factor).
         The from and to arguments give the new start and end of the viewport. */
@@ -525,7 +525,7 @@ declare module CodeMirror {
 
         /** Retrieves a list of all current selections. These will always be sorted, and never overlap (overlapping selections are merged).
         Each object in the array contains anchor and head properties referring to {line, ch} objects. */
-        listSelections(): { anchor: CodeMirror.Position; head: CodeMirror.Position }[];
+        listSelections(): CodeMirror.Selection[];
 
         /** Return true if any text is selected. */
         somethingSelected(): boolean;
@@ -534,7 +534,16 @@ declare module CodeMirror {
         setCursor(pos: CodeMirror.Position): void;
 
         /** Set the selection range.anchor and head should be { line , ch } objects.head defaults to anchor when not given. */
-        setSelection(anchor: CodeMirror.Position, head: CodeMirror.Position): void;
+        setSelection(anchor: CodeMirror.Position, head?: CodeMirror.Position, options?: any): void;
+
+        /**
+         * Sets a new set of selections. There must be at least one selection in the given array.
+         * When primary is a number, it determines which selection is the primary one.
+         * When it is not given, the primary index is taken from the previous selection,
+         * or set to the last range if the previous selection had less ranges than the new one.
+         * Supports the same options as setSelection.
+         */
+        setSelections(ranges: CodeMirror.Selection[], primary?: number, options?: any): void; 
 
         /** Similar to setSelection , but will, if shift is held or the extending flag is set,
         move the head of the selection while leaving the anchor at its current place.
@@ -689,6 +698,22 @@ declare module CodeMirror {
         line: number;
     }
 
+    interface Range {
+      from: Position
+      to: Position
+    }
+
+    interface Selection {
+      /**
+       * the fixed side of the selection
+       */
+      anchor: Position
+      /**
+       * the side of the selection that moves
+       */
+      head: Position
+    }
+
     interface EditorConfiguration {
         /** string| The starting value of the editor. Can be a string, or a document object. */
         value?: any;