--- 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);
},