Explorar o código

Merge pull request #1123 from blink1073/single-codemirror

Rendered codemirror widget
Jason Grout %!s(int64=8) %!d(string=hai) anos
pai
achega
0afe967117
Modificáronse 4 ficheiros con 214 adicións e 61 borrados
  1. 203 19
      src/codemirror/widget.ts
  2. 2 0
      src/console/content.ts
  3. 0 42
      src/console/panel.ts
  4. 9 0
      src/notebook/codemirror/index.css

+ 203 - 19
src/codemirror/widget.ts

@@ -5,11 +5,16 @@ import * as CodeMirror
   from 'codemirror';
 
 import 'codemirror/mode/meta';
+import 'codemirror/addon/runmode/runmode';
 
 import {
   Message
 } from 'phosphor/lib/core/messaging';
 
+import {
+  PanelLayout
+} from 'phosphor/lib/ui/panel';
+
 import {
   ResizeMessage, Widget
 } from 'phosphor/lib/ui/widget';
@@ -20,6 +25,15 @@ import {
  */
 const EDITOR_CLASS = 'jp-CodeMirrorWidget';
 
+/**
+ * The class name added to a live codemirror widget.
+ */
+const LIVE_CLASS = 'jp-CodeMirrorWidget-live';
+
+/**
+ * The class name added to a static codemirror widget.
+ */
+const STATIC_CLASS = 'jp-CodeMirrorWidget-static';
 
 /**
  * The name of the default CodeMirror theme
@@ -27,6 +41,7 @@ const EDITOR_CLASS = 'jp-CodeMirrorWidget';
 export
 const DEFAULT_CODEMIRROR_THEME = 'jupyter';
 
+
 /**
  * A widget which hosts a CodeMirror editor.
  */
@@ -39,48 +54,171 @@ class CodeMirrorWidget extends Widget {
   constructor(options: CodeMirror.EditorConfiguration = {}) {
     super();
     this.addClass(EDITOR_CLASS);
+    let layout = this.layout = new PanelLayout();
+    this.node.tabIndex = -1;
     options.theme = (options.theme || DEFAULT_CODEMIRROR_THEME);
-    this._editor = CodeMirror(this.node, options);
+    this._live = new LiveCodeMirror(options);
+    this._static = new StaticCodeMirror(this._live.editor);
+    layout.addWidget(this._static);
+    layout.addWidget(this._live);
+    this._live.hide();
   }
 
   /**
    * Dispose of the resources held by the widget.
    */
   dispose(): void {
-    this._editor = null;
+    this._static.dispose();
+    this._live.dispose();
     super.dispose();
   }
 
   /**
    * Get the editor wrapped by the widget.
+   */
+  get editor(): CodeMirror.Editor {
+    return this._live.editor;
+  }
+
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this._activate();
+  }
+
+  /**
+   * Handle the DOM events for the widget.
+   *
+   * @param event - The DOM event sent to the widget.
    *
    * #### Notes
-   * This is a ready-only property.
+   * 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.
    */
-   get editor(): CodeMirror.Editor {
-     return this._editor;
-   }
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'mousedown':
+      this._evtMouseDown(event as MouseEvent);
+      break;
+    case 'focus':
+      this._evtFocus(event as FocusEvent);
+      break;
+    case 'blur':
+      this._evtBlur(event as FocusEvent);
+      break;
+    default:
+      break;
+    }
+  }
 
   /**
    * A message handler invoked on an `'after-attach'` message.
    */
   protected onAfterAttach(msg: Message): void {
-    if (!this.isVisible) {
-      this._needsRefresh = true;
+    this.node.addEventListener('mousedown', this);
+    this.node.addEventListener('focus', this, true);
+    this.node.addEventListener('blur', this, true);
+  }
+
+  /**
+   * Handle `before_detach` messages for the widget.
+   */
+  protected onBeforeDetach(msg: Message): void {
+    this.node.removeEventListener('mousedown', this);
+    this.node.removeEventListener('focus', this, true);
+    this.node.removeEventListener('blur', this, true);
+  }
+
+  /**
+   * Handle `mousedown` events for the widget.
+   */
+  private _evtMouseDown(event: MouseEvent): void {
+    if (this._live.isVisible) {
       return;
     }
-    this._editor.refresh();
-    this._needsRefresh = false;
+    this._lastMouseDown = event;
+  }
+
+  /**
+   * Handle `focus` events for the widget.
+   */
+  private _evtFocus(event: FocusEvent): void {
+    this._activate();
+  }
+
+  /**
+   * Handle `blur` events for the widget.
+   */
+  private _evtBlur(event: FocusEvent): void {
+    this._lastMouseDown = null;
+    if (this.node.contains(event.relatedTarget as HTMLElement)) {
+      return;
+    }
+    this._live.hide();
+    this._static.show();
+  }
+
+  /**
+   * Handle an activation message or a focus event.
+   */
+  private _activate(): void {
+    let editor = this.editor;
+    if (editor.getOption('readOnly') !== false) {
+      this._lastMouseDown = null;
+      return;
+    }
+    this._static.hide();
+    this._live.show();
+    if (this._lastMouseDown) {
+      let x = this._lastMouseDown.clientX;
+      let y = this._lastMouseDown.clientY;
+      let pos = editor.coordsChar({ left: x, top: y });
+      editor.getDoc().setCursor(pos);
+    }
+    editor.focus();
+  }
+
+  private _live: LiveCodeMirror;
+  private _static: StaticCodeMirror;
+  private _lastMouseDown: MouseEvent;
+}
+
+
+/**
+ * A widget that hosts a codemirror instance.
+ */
+class LiveCodeMirror extends Widget {
+  /**
+   * Construct a live codemirror.
+   */
+  constructor(options: CodeMirror.EditorConfiguration) {
+    super();
+    this.addClass(LIVE_CLASS);
+    this._editor = CodeMirror(this.node, options);
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    this._editor = null;
+    super.dispose();
+  }
+
+  /**
+   * Get the editor wrapped by the widget.
+   */
+  get editor(): CodeMirror.Editor {
+     return this._editor;
   }
 
   /**
    * A message handler invoked on an `'after-show'` message.
    */
   protected onAfterShow(msg: Message): void {
-    if (this._needsRefresh) {
-      this._editor.refresh();
-      this._needsRefresh = false;
-    }
+    this._editor.refresh();
   }
 
   /**
@@ -92,16 +230,62 @@ class CodeMirrorWidget extends Widget {
     } else {
       this._editor.setSize(msg.width, msg.height);
     }
-    this._needsRefresh = false;
   }
 
+  private _editor: CodeMirror.Editor = null;
+}
+
+
+/**
+ * A widget that holds rendered codemirror text.
+ */
+class StaticCodeMirror extends Widget {
   /**
-   * Handle `'activate-request'` messages.
+   * Construct a new static code mirror widget.
    */
-  protected onActivateRequest(msg: Message): void {
-    this._editor.focus();
+  constructor(editor: CodeMirror.Editor) {
+    super({ node: document.createElement('pre') });
+    this._editor = editor;
+    this.addClass(`cm-s-${editor.getOption('theme')}`);
+    this.addClass('CodeMirror');
+    this.addClass(STATIC_CLASS);
+    CodeMirror.on(this._editor.getDoc(), 'change', (instance, change) => {
+      if (this.isVisible) {
+        this._render();
+      }
+    });
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    this._editor = null;
+    super.dispose();
+  }
+
+  /**
+   * A message handler invoked on an `'after-attach'` message.
+   */
+  protected onAfterAttach(msg: Message): void {
+    this._render();
+  }
+
+  /**
+   * A message handler invoked on an `'after-show'` message.
+   */
+  protected onAfterShow(msg: Message): void {
+    this._render();
+  }
+
+  /**
+   * Render the static content.
+   */
+  private _render(): void {
+    CodeMirror.runMode(this._editor.getDoc().getValue(),
+                       this._editor.getOption('mode'),
+                       this.node);
   }
 
   private _editor: CodeMirror.Editor = null;
-  private _needsRefresh = true;
 }

+ 2 - 0
src/console/content.ts

@@ -113,6 +113,8 @@ class ConsoleContent extends Widget {
     // Create the panels that hold the content and input.
     let layout = this.layout = new PanelLayout();
     this._content = new Panel();
+    this._content.node.tabIndex = -1;
+    this._content.node.style.outline = 'none';
     this._input = new Panel();
     this._content.addClass(CONTENT_CLASS);
     this._input.addClass(INPUT_CLASS);

+ 0 - 42
src/console/panel.ts

@@ -78,41 +78,6 @@ class ConsolePanel extends Panel {
     super.dispose();
   }
 
-  /**
-   * 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 console panel's node. It should
-   * not be called directly by user code.
-   */
-  handleEvent(event: Event): void {
-    switch (event.type) {
-    case 'mousedown':
-      this._evtMouseDown(event as MouseEvent);
-      break;
-    default:
-      break;
-    }
-  }
-
-  /**
-   * Handle `after_attach` messages for the widget.
-   */
-  protected onAfterAttach(msg: Message): void {
-    super.onAfterAttach(msg);
-    this.node.addEventListener('mousedown', this);
-  }
-
-  /**
-   * Handle `before_detach` messages for the widget.
-   */
-  protected onBeforeDetach(msg: Message): void {
-    this.node.removeEventListener('mousedown', this);
-  }
-
   /**
    * Handle `'activate-request'` messages.
    */
@@ -148,13 +113,6 @@ class ConsolePanel extends Panel {
     });
   }
 
-  /**
-   * Handle `mousedown` events for the widget.
-   */
-  private _evtMouseDown(event: MouseEvent): void {
-    this.activate();
-  }
-
   private _content: ConsoleContent = null;
 }
 

+ 9 - 0
src/notebook/codemirror/index.css

@@ -33,3 +33,12 @@
   padding: 0 4px 0 4px;
 }
 
+
+.jp-CodeMirrorWidget-static {
+  margin: var(--jp-code-padding);
+}
+
+
+.jp-CodeMirrorWidget, .jp-CodeMirrorWidget-static {
+  cursor: text;
+}