Bug 1295213 - expose Codemirror instance and allow direct embedding in devtools r=bgrins,gl
authorJames Long <longster@gmail.com>
Thu, 08 Sep 2016 10:42:43 -0400
changeset 313216 3a44273280cf5dd2a761c91fb9dac626c9d0700d
parent 313215 a3a5d19b2d0539c66e787e0f2f86c539fb1f6fd8
child 313217 c46176e993d5f4b344e12189671af1753bbfe106
push id30675
push userkwierso@gmail.com
push dateThu, 08 Sep 2016 22:10:56 +0000
treeherdermozilla-central@e6e733f522a5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, gl
bugs1295213
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1295213 - expose Codemirror instance and allow direct embedding in devtools r=bgrins,gl
devtools/client/sourceeditor/editor.js
--- a/devtools/client/sourceeditor/editor.js
+++ b/devtools/client/sourceeditor/editor.js
@@ -46,17 +46,16 @@ const { OS } = Services.appinfo;
 
 const CM_STYLES = [
   "chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css",
   "chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css",
   "chrome://devtools/content/sourceeditor/codemirror/mozilla.css"
 ];
 
 const CM_SCRIPTS = [
-  "chrome://devtools/content/shared/theme-switching.js",
   "chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/search/searchcursor.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/search/search.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/edit/matchbrackets.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/edit/closebrackets.js",
   "chrome://devtools/content/sourceeditor/codemirror/addon/comment/comment.js",
   "chrome://devtools/content/sourceeditor/codemirror/mode/javascript.js",
@@ -248,16 +247,31 @@ function Editor(config) {
 
 Editor.prototype = {
   container: null,
   version: null,
   config: null,
   Doc: null,
 
   /**
+   * Exposes the CodeMirror instance. We want to get away from trying to
+   * abstract away the API entirely, and this makes it easier to integrate in
+   * various environments and do complex things.
+   */
+  get codeMirror() {
+    if (!editors.has(this)) {
+      throw new Error(
+        "CodeMirror instance does not exist. You must wait " +
+          "for it to be appended to the DOM."
+      );
+    }
+    return editors.get(this);
+  },
+
+  /**
    * Appends the current Editor instance to the element specified by
    * 'el'. You can also provide your won iframe to host the editor as
    * an optional second parameter. This method actually creates and
    * loads CodeMirror and all its dependencies.
    *
    * This method is asynchronous and returns a promise.
    */
   appendTo: function (el, env) {
@@ -270,221 +284,237 @@ Editor.prototype = {
 
     env.flex = 1;
 
     if (cm) {
       throw new Error("You can append an editor only once.");
     }
 
     let onLoad = () => {
-      // Once the iframe is loaded, we can inject CodeMirror
-      // and its dependencies into its DOM.
-
-      env.removeEventListener("load", onLoad, true);
       let win = env.contentWindow.wrappedJSObject;
 
       if (!this.config.themeSwitching) {
         win.document.documentElement.setAttribute("force-theme", "light");
       }
 
-      let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
-      scriptsToInject.forEach(url => {
-        if (url.startsWith("chrome://")) {
-          Services.scriptloader.loadSubScript(url, win, "utf8");
-        }
-      });
-      // Replace the propertyKeywords, colorKeywords and valueKeywords
-      // properties of the CSS MIME type with the values provided by the CSS properties
-      // database.
-
-      const {
-        propertyKeywords,
-        colorKeywords,
-        valueKeywords
-      } = getCSSKeywords(this.config.cssProperties);
-
-      let cssSpec = win.CodeMirror.resolveMode("text/css");
-      cssSpec.propertyKeywords = propertyKeywords;
-      cssSpec.colorKeywords = colorKeywords;
-      cssSpec.valueKeywords = valueKeywords;
-      win.CodeMirror.defineMIME("text/css", cssSpec);
-
-      let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
-      scssSpec.propertyKeywords = propertyKeywords;
-      scssSpec.colorKeywords = colorKeywords;
-      scssSpec.valueKeywords = valueKeywords;
-      win.CodeMirror.defineMIME("text/x-scss", scssSpec);
-
-      win.CodeMirror.commands.save = () => this.emit("saveRequested");
-
-      // Create a CodeMirror instance add support for context menus,
-      // overwrite the default controller (otherwise items in the top and
-      // context menus won't work).
-
-      cm = win.CodeMirror(win.document.body, this.config);
-      this.Doc = win.CodeMirror.Doc;
-
-      // Disable APZ for source editors. It currently causes the line numbers to
-      // "tear off" and swim around on top of the content. Bug 1160601 tracks
-      // finding a solution that allows APZ to work with CodeMirror.
-      cm.getScrollerElement().addEventListener("wheel", ev => {
-        // By handling the wheel events ourselves, we force the platform to
-        // scroll synchronously, like it did before APZ. However, we lose smooth
-        // scrolling for users with mouse wheels. This seems acceptible vs.
-        // doing nothing and letting the gutter slide around.
-        ev.preventDefault();
-
-        let { deltaX, deltaY } = ev;
-
-        if (ev.deltaMode == ev.DOM_DELTA_LINE) {
-          deltaX *= cm.defaultCharWidth();
-          deltaY *= cm.defaultTextHeight();
-        } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
-          deltaX *= cm.getWrapperElement().clientWidth;
-          deltaY *= cm.getWrapperElement().clientHeight;
-        }
-
-        cm.getScrollerElement().scrollBy(deltaX, deltaY);
-      });
-
-      cm.getWrapperElement().addEventListener("contextmenu", ev => {
-        ev.preventDefault();
-
-        if (!this.config.contextMenu) {
-          return;
-        }
-
-        let popup = this.config.contextMenu;
-        if (typeof popup == "string") {
-          popup = el.ownerDocument.getElementById(this.config.contextMenu);
-        }
-
-        this.emit("popupOpen", ev, popup);
-        popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
-      }, false);
-
-      // Intercept the find and find again keystroke on CodeMirror, to avoid
-      // the browser's search
-
-      let findKey = L10N.getStr("find.commandkey");
-      let findAgainKey = L10N.getStr("findAgain.commandkey");
-      let [accel, modifier] = OS === "Darwin"
-                                      ? ["metaKey", "altKey"]
-                                      : ["ctrlKey", "shiftKey"];
-
-      cm.getWrapperElement().addEventListener("keydown", ev => {
-        let key = ev.key.toUpperCase();
-        let node = ev.originalTarget;
-        let isInput = node.tagName === "INPUT";
-        let isSearchInput = isInput && node.type === "search";
-
-        // replace box is a different input instance than search, and it is
-        // located in a code mirror dialog
-        let isDialogInput = isInput &&
-                       node.parentNode &&
-                       node.parentNode.classList.contains("CodeMirror-dialog");
-
-        if (!ev[accel] || !(isSearchInput || isDialogInput)) {
-          return;
-        }
-
-        if (key === findKey) {
-          ev.preventDefault();
-
-          if (isSearchInput || ev[modifier]) {
-            node.select();
-          }
-        } else if (key === findAgainKey) {
-          ev.preventDefault();
-
-          if (!isSearchInput) {
-            return;
-          }
-
-          let query = node.value;
-
-          // If there isn't a search state, or the text in the input does not
-          // match with the current search state, we need to create a new one
-          if (!cm.state.search || cm.state.search.query !== query) {
-            cm.state.search = {
-              posFrom: null,
-              posTo: null,
-              overlay: null,
-              query
-            };
-          }
-
-          if (ev.shiftKey) {
-            cm.execCommand("findPrev");
-          } else {
-            cm.execCommand("findNext");
-          }
-        }
-      });
-
-      cm.on("focus", () => this.emit("focus"));
-      cm.on("scroll", () => this.emit("scroll"));
-      cm.on("change", () => {
-        this.emit("change");
-        if (!this._lastDirty) {
-          this._lastDirty = true;
-          this.emit("dirty-change");
-        }
-      });
-      cm.on("cursorActivity", () => this.emit("cursorActivity"));
-
-      cm.on("gutterClick", (cmArg, line, gutter, ev) => {
-        let head = { line: line, ch: 0 };
-        let tail = { line: line, ch: this.getText(line).length };
-
-        // Shift-click on a gutter selects the whole line.
-        if (ev.shiftKey) {
-          cmArg.setSelection(head, tail);
-          return;
-        }
-
-        this.emit("gutterClick", line, ev.button);
-      });
-
-      win.CodeMirror.defineExtension("l10n", (name) => {
-        return L10N.getStr(name);
-      });
-
-      cm.getInputField().controllers.insertControllerAt(0, controller(this));
-
+      Services.scriptloader.loadSubScript(
+        "chrome://devtools/content/shared/theme-switching.js",
+        win, "utf8"
+      );
       this.container = env;
-      editors.set(this, cm);
-
-      this.reloadPreferences = this.reloadPreferences.bind(this);
-      this._prefObserver = new PrefObserver("devtools.editor.");
-      this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
-      this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
-      this._prefObserver.on(KEYMAP, this.reloadPreferences);
-      this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
-      this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
-      this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
-      this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
-
-      this.reloadPreferences();
-
-      win.editor = this;
-      let editorReadyEvent = new win.CustomEvent("editorReady");
-      win.dispatchEvent(editorReadyEvent);
+      this._setup(win.document.body);
+      env.removeEventListener("load", onLoad, true);
 
       def.resolve();
     };
 
     env.addEventListener("load", onLoad, true);
     env.setAttribute("src", CM_IFRAME);
     el.appendChild(env);
 
     this.once("destroy", () => el.removeChild(env));
     return def.promise;
   },
 
+  appendToLocalElement: function (el) {
+    this._setup(el);
+  },
+
+  /**
+   * Do the actual appending and configuring of the CodeMirror instance. This is
+   * used by both append functions above, and does all the hard work to
+   * configure CodeMirror with all the right options/modes/etc.
+   */
+  _setup: function (el) {
+    let win = el.ownerDocument.defaultView;
+
+    let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
+    scriptsToInject.forEach(url => {
+      if (url.startsWith("chrome://")) {
+        Services.scriptloader.loadSubScript(url, win, "utf8");
+      }
+    });
+
+    // Replace the propertyKeywords, colorKeywords and valueKeywords
+    // properties of the CSS MIME type with the values provided by the CSS properties
+    // database.
+    const {
+      propertyKeywords,
+      colorKeywords,
+      valueKeywords
+    } = getCSSKeywords(this.config.cssProperties);
+
+    let cssSpec = win.CodeMirror.resolveMode("text/css");
+    cssSpec.propertyKeywords = propertyKeywords;
+    cssSpec.colorKeywords = colorKeywords;
+    cssSpec.valueKeywords = valueKeywords;
+    win.CodeMirror.defineMIME("text/css", cssSpec);
+
+    let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
+    scssSpec.propertyKeywords = propertyKeywords;
+    scssSpec.colorKeywords = colorKeywords;
+    scssSpec.valueKeywords = valueKeywords;
+    win.CodeMirror.defineMIME("text/x-scss", scssSpec);
+
+    win.CodeMirror.commands.save = () => this.emit("saveRequested");
+
+    // Create a CodeMirror instance add support for context menus,
+    // overwrite the default controller (otherwise items in the top and
+    // context menus won't work).
+
+    let cm = win.CodeMirror(el, this.config);
+    this.Doc = win.CodeMirror.Doc;
+
+    // Disable APZ for source editors. It currently causes the line numbers to
+    // "tear off" and swim around on top of the content. Bug 1160601 tracks
+    // finding a solution that allows APZ to work with CodeMirror.
+    cm.getScrollerElement().addEventListener("wheel", ev => {
+      // By handling the wheel events ourselves, we force the platform to
+      // scroll synchronously, like it did before APZ. However, we lose smooth
+      // scrolling for users with mouse wheels. This seems acceptible vs.
+      // doing nothing and letting the gutter slide around.
+      ev.preventDefault();
+
+      let { deltaX, deltaY } = ev;
+
+      if (ev.deltaMode == ev.DOM_DELTA_LINE) {
+        deltaX *= cm.defaultCharWidth();
+        deltaY *= cm.defaultTextHeight();
+      } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
+        deltaX *= cm.getWrapperElement().clientWidth;
+        deltaY *= cm.getWrapperElement().clientHeight;
+      }
+
+      cm.getScrollerElement().scrollBy(deltaX, deltaY);
+    });
+
+    cm.getWrapperElement().addEventListener("contextmenu", ev => {
+      ev.preventDefault();
+
+      if (!this.config.contextMenu) {
+        return;
+      }
+
+      let popup = this.config.contextMenu;
+      if (typeof popup == "string") {
+        popup = el.ownerDocument.getElementById(this.config.contextMenu);
+      }
+
+      this.emit("popupOpen", ev, popup);
+      popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
+    }, false);
+
+    // Intercept the find and find again keystroke on CodeMirror, to avoid
+    // the browser's search
+
+    let findKey = L10N.getStr("find.commandkey");
+    let findAgainKey = L10N.getStr("findAgain.commandkey");
+    let [accel, modifier] = OS === "Darwin"
+        ? ["metaKey", "altKey"]
+        : ["ctrlKey", "shiftKey"];
+
+    cm.getWrapperElement().addEventListener("keydown", ev => {
+      let key = ev.key.toUpperCase();
+      let node = ev.originalTarget;
+      let isInput = node.tagName === "INPUT";
+      let isSearchInput = isInput && node.type === "search";
+
+      // replace box is a different input instance than search, and it is
+      // located in a code mirror dialog
+      let isDialogInput = isInput &&
+          node.parentNode &&
+          node.parentNode.classList.contains("CodeMirror-dialog");
+
+      if (!ev[accel] || !(isSearchInput || isDialogInput)) {
+        return;
+      }
+
+      if (key === findKey) {
+        ev.preventDefault();
+
+        if (isSearchInput || ev[modifier]) {
+          node.select();
+        }
+      } else if (key === findAgainKey) {
+        ev.preventDefault();
+
+        if (!isSearchInput) {
+          return;
+        }
+
+        let query = node.value;
+
+        // If there isn't a search state, or the text in the input does not
+        // match with the current search state, we need to create a new one
+        if (!cm.state.search || cm.state.search.query !== query) {
+          cm.state.search = {
+            posFrom: null,
+            posTo: null,
+            overlay: null,
+            query
+          };
+        }
+
+        if (ev.shiftKey) {
+          cm.execCommand("findPrev");
+        } else {
+          cm.execCommand("findNext");
+        }
+      }
+    });
+
+    cm.on("focus", () => this.emit("focus"));
+    cm.on("scroll", () => this.emit("scroll"));
+    cm.on("change", () => {
+      this.emit("change");
+      if (!this._lastDirty) {
+        this._lastDirty = true;
+        this.emit("dirty-change");
+      }
+    });
+    cm.on("cursorActivity", () => this.emit("cursorActivity"));
+
+    cm.on("gutterClick", (cmArg, line, gutter, ev) => {
+      let head = { line: line, ch: 0 };
+      let tail = { line: line, ch: this.getText(line).length };
+
+      // Shift-click on a gutter selects the whole line.
+      if (ev.shiftKey) {
+        cmArg.setSelection(head, tail);
+        return;
+      }
+
+      this.emit("gutterClick", line, ev.button);
+    });
+
+    win.CodeMirror.defineExtension("l10n", (name) => {
+      return L10N.getStr(name);
+    });
+
+    cm.getInputField().controllers.insertControllerAt(0, controller(this));
+
+    editors.set(this, cm);
+
+    this.reloadPreferences = this.reloadPreferences.bind(this);
+    this._prefObserver = new PrefObserver("devtools.editor.");
+    this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
+    this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
+    this._prefObserver.on(KEYMAP, this.reloadPreferences);
+    this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
+    this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
+    this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
+    this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
+
+    this.reloadPreferences();
+
+    win.editor = this;
+    let editorReadyEvent = new win.CustomEvent("editorReady");
+    win.dispatchEvent(editorReadyEvent);
+  },
+
   /**
    * Returns a boolean indicating whether the editor is ready to
    * use. Use appendTo(el).then(() => {}) for most cases
    */
   isAppended: function () {
     return editors.has(this);
   },