Bug 1295213 - expose Codemirror instance and allow direct embedding in devtools r=bgrins draft
authorJames Long <longster@gmail.com>
Wed, 07 Sep 2016 09:36:21 -0700
changeset 411109 b7b970fd91cb6065905226c43c332cfaac4b3aae
parent 410996 041a925171e431bf51fb50193ab19d156088c89a
child 530674 56c16b4fb57a355b647dc46b1306f9c74cd1fee9
push id28838
push userbgrinstead@mozilla.com
push dateWed, 07 Sep 2016 16:36:34 +0000
reviewersbgrins
bugs1295213
milestone51.0a1
Bug 1295213 - expose Codemirror instance and allow direct embedding in devtools r=bgrins MozReview-Commit-ID: 7AsdeqvjdNl
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",
@@ -247,16 +246,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.
    */
@@ -270,221 +284,232 @@ 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));
-
-      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);
+      Services.scriptloader.loadSubScript(
+        "chrome://devtools/content/shared/theme-switching.js",
+        win, "utf8"
+      );
+      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(el) {
+    this.setup(el);
+  },
+
+  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).
+
+    cm = win.CodeMirror(targetEl, 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));
+
+    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);
+  },
+
   /**
    * Returns a boolean indicating whether the editor is ready to
    * use. Use appendTo(el).then(() => {}) for most cases
    */
   isAppended: function () {
     return editors.has(this);
   },