Procházet zdrojové kódy

Create a cell editor widget

Steven Silvester před 8 roky
rodič
revize
822be64ede

+ 234 - 0
src/notebook/cells/editor.ts

@@ -0,0 +1,234 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import * as CodeMirror
+  from 'codemirror';
+
+import {
+  CodeMirrorWidget
+} from 'jupyter-js-ui/lib/codemirror/widget';
+
+import {
+  ISignal, Signal
+} from 'phosphor-signaling';
+
+import {
+  ICellModel
+} from './model';
+
+
+/**
+ * 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 location of requested edges.
+ */
+export
+type EdgeLocation = 'top' | 'bottom';
+
+/**
+ * The class name added to cell editor widget nodes.
+ */
+const CELL_EDITOR_CLASS = 'jp-CellEditor';
+
+
+/**
+ * An interface describing editor text changes.
+ */
+export
+interface ITextChange {
+  /**
+   * The character number of the editor cursor within a line.
+   */
+  ch: number;
+
+  /**
+   * The height of a character in the editor.
+   */
+  chHeight: number;
+
+  /**
+   * The width of a character in the editor.
+   */
+  chWidth: number;
+
+  /**
+   * The line number of the editor cursor.
+   */
+  line: number;
+
+  /**
+   * The coordinate position of the cursor.
+   */
+  coords: { left: number; right: number; top: number; bottom: number; };
+
+  /**
+   * The old value of the editor text.
+   */
+  oldValue: string;
+
+  /**
+   * The new value of the editor text.
+   */
+  newValue: string;
+}
+
+
+/**
+ * A widget for a cell editor.
+ */
+export
+class CellEditorWidget extends CodeMirrorWidget {
+  /**
+   * Construct a new cell editor widget.
+   */
+  constructor(model: ICellModel) {
+    super();
+    this.addClass(CELL_EDITOR_CLASS);
+    this._model = model;
+    let editor = this.editor;
+    let doc = editor.getDoc();
+    CodeMirror.on(doc, 'change', (instance, change) => {
+      this.onDocChange(instance, change);
+    });
+    CodeMirror.on(editor, 'keydown', (instance, evt) => {
+      this.onEditorKeydown(instance, evt);
+    });
+    doc.setValue(model.source);
+  }
+
+  /**
+   * A signal emitted when either the top or bottom edge is requested.
+   */
+  get edgeRequested(): ISignal<CellEditorWidget, EdgeLocation> {
+    return Private.edgeRequestedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when a text change is completed.
+   */
+  get textChanged(): ISignal<CellEditorWidget, ITextChange> {
+    return Private.textChangedSignal.bind(this);
+  }
+
+  /**
+   * Get the cell model used by the editor.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get model(): ICellModel {
+    return this._model;
+  }
+
+  /**
+   * Dispose of the resources held by the editor.
+   */
+  dispose(): void {
+    this._model = null;
+    super.dispose();
+  }
+
+  /**
+   * Get the current cursor position of the editor.
+   */
+  getCursorPosition(): number {
+    let doc = this.editor.getDoc();
+    let position = doc.getCursor();
+    return doc.indexFromPos(position);
+  }
+
+  /**
+   * Handle changes in the model.
+   */
+  protected onModelChanged(model: ICellModel, change: string): void {
+    switch (change) {
+    case 'source':
+      let doc = this.editor.getDoc();
+      let value = doc.getValue();
+      if (value !== model.source) {
+        doc.setValue(model.source);
+      }
+      break;
+    default:
+      break;
+    }
+  }
+
+  /**
+   * Handle change events from the document.
+   */
+  protected onDocChange(doc: CodeMirror.Doc, change: CodeMirror.EditorChange): void {
+    if (change.origin === 'setValue') {
+      return;
+    }
+    let model = this.model;
+    let editor = this.editor;
+    let oldValue = model.source;
+    let newValue = doc.getValue();
+    if (oldValue === newValue) {
+      return;
+    }
+    model.source = newValue;
+
+    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');
+    this.textChanged.emit({
+      line, ch, chHeight, chWidth, coords, 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 (line === 0 && ch === 0 && event.keyCode === UP_ARROW) {
+      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) {
+      this.edgeRequested.emit('bottom');
+      return;
+    }
+  }
+
+  private _model: ICellModel = null;
+}
+
+
+/**
+ * A namespace for private data.
+ */
+namespace Private {
+  /**
+   * A signal emitted when either the top or bottom edge is requested.
+   */
+  export
+  const edgeRequestedSignal = new Signal<CellEditorWidget, EdgeLocation>();
+
+  /**
+   * A signal emitted when a text change is completed.
+   */
+  export
+  const textChangedSignal = new Signal<CellEditorWidget, ITextChange>();
+}

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

@@ -2,17 +2,10 @@
 // Distributed under the terms of the Modified BSD License.
 // Distributed under the terms of the Modified BSD License.
 'use strict';
 'use strict';
 
 
-import * as CodeMirror
-  from 'codemirror';
-
 import {
 import {
   loadModeByMIME
   loadModeByMIME
 } from 'jupyter-js-ui/lib/codemirror';
 } from 'jupyter-js-ui/lib/codemirror';
 
 
-import {
-  CodeMirrorWidget
-} from 'jupyter-js-ui/lib/codemirror/widget';
-
 import {
 import {
   RenderMime
   RenderMime
 } from 'jupyter-js-ui/lib/rendermime';
 } from 'jupyter-js-ui/lib/rendermime';
@@ -45,6 +38,10 @@ import {
   IMetadataCursor
   IMetadataCursor
 } from '../common/metadata';
 } from '../common/metadata';
 
 
+import {
+  CellEditorWidget
+} from './editor';
+
 import {
 import {
   ICodeCellModel, ICellModel
   ICodeCellModel, ICellModel
 } from './model';
 } from './model';
@@ -70,11 +67,6 @@ const PROMPT_CLASS = 'jp-InputArea-prompt';
  */
  */
 const EDITOR_CLASS = 'jp-InputArea-editor';
 const EDITOR_CLASS = 'jp-InputArea-editor';
 
 
-/**
- * The class name added to a codemirror widget.
- */
-const CODEMIRROR_CLASS = 'jp-CodeMirror';
-
 /**
 /**
  * The class name added to the cell when collapsed.
  * The class name added to the cell when collapsed.
  */
  */
@@ -121,6 +113,13 @@ const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
  */
  */
 export
 export
 class BaseCellWidget extends Widget {
 class BaseCellWidget extends Widget {
+  /**
+   * Create a new cell editor widget.
+   */
+  static createCellEditor(model: ICellModel): CellEditorWidget {
+    return new CellEditorWidget(model);
+  }
+
   /**
   /**
    * Construct a new base cell widget.
    * Construct a new base cell widget.
    */
    */
@@ -128,15 +127,14 @@ class BaseCellWidget extends Widget {
     super();
     super();
     this.addClass(CELL_CLASS);
     this.addClass(CELL_CLASS);
     this._model = model;
     this._model = model;
-    this._editor = new CodeMirrorWidget();
+    let ctor = this.constructor as typeof BaseCellWidget;
+    this._editor = ctor.createCellEditor(model);
     this._input = new InputAreaWidget(this._editor);
     this._input = new InputAreaWidget(this._editor);
     this.layout = new PanelLayout();
     this.layout = new PanelLayout();
     (this.layout as PanelLayout).addChild(this._input);
     (this.layout as PanelLayout).addChild(this._input);
-    this._initializeEditor();
     model.contentChanged.connect(this.onModelChanged, this);
     model.contentChanged.connect(this.onModelChanged, this);
     this._trustedCursor = model.getMetadata('trusted');
     this._trustedCursor = model.getMetadata('trusted');
     this._trusted = this._trustedCursor.getValue();
     this._trusted = this._trustedCursor.getValue();
-    this.editor.getDoc().setValue(model.source);
   }
   }
 
 
   /**
   /**
@@ -150,14 +148,14 @@ class BaseCellWidget extends Widget {
   }
   }
 
 
   /**
   /**
-   * Get the editor used by the widget.
+   * Get the editor widget used by the cell.
    *
    *
    * #### Notes
    * #### Notes
    * This is a ready-only property.
    * This is a ready-only property.
    */
    */
-   get editor(): CodeMirror.Editor {
-     return this._editor.editor;
-   }
+  get editor(): CellEditorWidget {
+    return this._editor;
+  }
 
 
   /**
   /**
    * The mimetype used by the cell.
    * The mimetype used by the cell.
@@ -170,7 +168,7 @@ class BaseCellWidget extends Widget {
       return;
       return;
     }
     }
     this._mimetype = value;
     this._mimetype = value;
-    loadModeByMIME(this.editor, value);
+    loadModeByMIME(this.editor.editor, value);
   }
   }
 
 
   /**
   /**
@@ -201,16 +199,7 @@ class BaseCellWidget extends Widget {
    * Focus the widget.
    * Focus the widget.
    */
    */
   focus(): void {
   focus(): void {
-    this.editor.focus();
-  }
-
-  /**
-   * Get the current cursor position of the editor.
-   */
-  getCursorPosition(): number {
-    let doc = this.editor.getDoc();
-    let position = doc.getCursor();
-    return doc.indexFromPos(position);
+    this.editor.editor.focus();
   }
   }
 
 
   /**
   /**
@@ -261,7 +250,7 @@ class BaseCellWidget extends Widget {
   protected onUpdateRequest(message: Message): void {
   protected onUpdateRequest(message: Message): void {
     // Handle read only state.
     // Handle read only state.
     let option = this._readOnly ? 'nocursor' : false;
     let option = this._readOnly ? 'nocursor' : false;
-    this.editor.setOption('readOnly', option);
+    this.editor.editor.setOption('readOnly', option);
     this.toggleClass(READONLY_CLASS, this._readOnly);
     this.toggleClass(READONLY_CLASS, this._readOnly);
   }
   }
 
 
@@ -270,13 +259,6 @@ class BaseCellWidget extends Widget {
    */
    */
   protected onModelChanged(model: ICellModel, change: string): void {
   protected onModelChanged(model: ICellModel, change: string): void {
     switch (change) {
     switch (change) {
-    case 'source':
-      let doc = this.editor.getDoc();
-      let value = doc.getValue();
-      if (value !== model.source) {
-        doc.setValue(model.source);
-      }
-      break;
     case 'metadata':
     case 'metadata':
     case 'metadata.trusted':
     case 'metadata.trusted':
       this._trusted = this._trustedCursor.getValue();
       this._trusted = this._trustedCursor.getValue();
@@ -287,21 +269,8 @@ class BaseCellWidget extends Widget {
     }
     }
   }
   }
 
 
-  /**
-   * Initialize the codemirror editor.
-   */
-  private _initializeEditor(): void {
-    let doc = this.editor.getDoc();
-    CodeMirror.on(doc, 'change', (instance, change) => {
-      if (change.origin === 'setValue') {
-        return;
-      }
-      this._model.source = instance.getValue();
-    });
-  }
-
   private _input: InputAreaWidget = null;
   private _input: InputAreaWidget = null;
-  private _editor: CodeMirrorWidget = null;
+  private _editor: CellEditorWidget = null;
   private _model: ICellModel = null;
   private _model: ICellModel = null;
   private _mimetype = 'text/plain';
   private _mimetype = 'text/plain';
   private _readOnly = false;
   private _readOnly = false;
@@ -498,11 +467,10 @@ class InputAreaWidget extends Widget {
   /**
   /**
    * Construct an input area widget.
    * Construct an input area widget.
    */
    */
-  constructor(editor: CodeMirrorWidget) {
+  constructor(editor: CellEditorWidget) {
     super();
     super();
     this.addClass(INPUT_CLASS);
     this.addClass(INPUT_CLASS);
     editor.addClass(EDITOR_CLASS);
     editor.addClass(EDITOR_CLASS);
-    editor.addClass(CODEMIRROR_CLASS);
     this.layout = new PanelLayout();
     this.layout = new PanelLayout();
     let prompt = new Widget();
     let prompt = new Widget();
     prompt.addClass(PROMPT_CLASS);
     prompt.addClass(PROMPT_CLASS);

+ 1 - 1
src/notebook/notebook/actions.ts

@@ -52,7 +52,7 @@ namespace NotebookActions {
     let nbModel = widget.model;
     let nbModel = widget.model;
     let index = widget.activeCellIndex;
     let index = widget.activeCellIndex;
     let child = widget.childAt(index);
     let child = widget.childAt(index);
-    let position = child.getCursorPosition();
+    let position = child.editor.getCursorPosition();
     let orig = child.model.source;
     let orig = child.model.source;
 
 
     // Create new models to preserve history.
     // Create new models to preserve history.

+ 8 - 8
src/notebook/theme.css

@@ -42,7 +42,7 @@
 }
 }
 
 
 
 
-.jp-CodeMirror {
+.jp-CellEditor {
   border: 1px solid #cfcfcf;
   border: 1px solid #cfcfcf;
   border-radius: 2px;
   border-radius: 2px;
   background: #f7f7f7;
   background: #f7f7f7;
@@ -284,7 +284,7 @@
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror {
+.jp-CellEditor > .CodeMirror {
   line-height: 1.21429em;
   line-height: 1.21429em;
   /* Changed from 1em to our global default */
   /* Changed from 1em to our global default */
   font-size: 14px;
   font-size: 14px;
@@ -295,7 +295,7 @@
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror-scroll {
+.jp-CellEditor > .CodeMirror-scroll {
   /*  The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/
   /*  The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/
   /*  We have found that if it is visible, vertical scrollbars appear with font size changes.*/
   /*  We have found that if it is visible, vertical scrollbars appear with font size changes.*/
   overflow-y: hidden;
   overflow-y: hidden;
@@ -303,25 +303,25 @@
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror-lines {
+.jp-CellEditor > .CodeMirror-lines {
   /* In CM2, this used to be 0.4em, but in CM3 it went to 4px. We need the em value because */
   /* In CM2, this used to be 0.4em, but in CM3 it went to 4px. We need the em value because */
   /* we have set a different line-height and want this to scale with that. */
   /* we have set a different line-height and want this to scale with that. */
   padding: 0.4em;
   padding: 0.4em;
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror-linenumber {
+.jp-CellEditor > .CodeMirror-linenumber {
   padding: 0 8px 0 4px;
   padding: 0 8px 0 4px;
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror-gutters {
+.jp-CellEditor > .CodeMirror-gutters {
   border-bottom-left-radius: 2px;
   border-bottom-left-radius: 2px;
   border-top-left-radius: 2px;
   border-top-left-radius: 2px;
 }
 }
 
 
 
 
-.jp-CodeMirror > .CodeMirror pre {
+.jp-CellEditor > .CodeMirror pre {
   /* In CM3 this went to 4px from 0 in CM2. We need the 0 value because of how we size */
   /* In CM3 this went to 4px from 0 in CM2. We need the 0 value because of how we size */
   /* .CodeMirror-lines */
   /* .CodeMirror-lines */
   padding: 0;
   padding: 0;
@@ -336,7 +336,7 @@
 }
 }
 
 
 
 
-.jp-Console .jp-InputArea-editor.jp-CodeMirror {
+.jp-Console .jp-InputArea-editor.jp-CellEditor {
   background: transparent;
   background: transparent;
   border-color: transparent;
   border-color: transparent;
 }
 }

+ 6 - 0
typings/codemirror/codemirror.d.ts

@@ -56,6 +56,12 @@ declare module CodeMirror {
     function on(element: any, eventName: string, handler: Function): void;
     function on(element: any, eventName: string, handler: Function): void;
     function off(element: any, eventName: string, handler: Function): void;
     function off(element: any, eventName: string, handler: Function): void;
 
 
+    /**
+     * Fired on a keydown event on the editor.
+     */
+    function on(editor: Editor, eventName: 'keydown', handler: (instance: Editor, event: KeyboardEvent) => void): void;
+    function off(editor: Editor, eventName: 'keydown', handler: (instance: Editor, event: KeyboardEvent) => void): void;
+
     /** Fired whenever a change occurs to the document. changeObj has a similar type as the object passed to the editor's "change" event,
     /** Fired whenever a change occurs to the document. changeObj has a similar type as the object passed to the editor's "change" event,
     but it never has a next property, because document change events are not batched (whereas editor change events are). */
     but it never has a next property, because document change events are not batched (whereas editor change events are). */
     function on(doc: Doc, eventName: 'change', handler: (instance: Doc, change: EditorChange) => void ): void;
     function on(doc: Doc, eventName: 'change', handler: (instance: Doc, change: EditorChange) => void ): void;