Bug 919978 - Make StyleEditor use CodeMirror, r=anton, msucan
authorGirish Sharma <scrapmachines@gmail.com>
Thu, 24 Oct 2013 10:31:02 +0530
changeset 165878 d6018de7fe82b2e584d17d237b2500aada5c202d
parent 165877 e095e2f442f847d77fe563623d0f58f87b7ae643
child 165879 a884fbbc334ae339ac6c9d631a3e8290b4166850
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanton, msucan
bugs919978
milestone27.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 919978 - Make StyleEditor use CodeMirror, r=anton, msucan
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/debugger/debugger-view.js
browser/devtools/jar.mn
browser/devtools/sourceeditor/codemirror/README
browser/devtools/sourceeditor/codemirror/closebrackets.js
browser/devtools/sourceeditor/debugger.js
browser/devtools/sourceeditor/editor.js
browser/devtools/styleeditor/StyleEditorUI.jsm
browser/devtools/styleeditor/StyleSheetEditor.jsm
browser/devtools/styleeditor/test/browser_styleeditor_new.js
browser/devtools/styleeditor/test/browser_styleeditor_reload.js
browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -857,17 +857,17 @@ FilterView.prototype = {
    * (Jump to lines in the currently visible source).
    *
    * @param number aLine
    *        The source line number to jump to.
    */
   _performLineSearch: function(aLine) {
     // Make sure we're actually searching for a valid line.
     if (aLine) {
-      DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 });
+      DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center");
     }
   },
 
   /**
    * Performs a token search if necessary.
    * (Search for tokens in the currently visible source).
    *
    * @param string aToken
@@ -1492,16 +1492,17 @@ FilteredFunctionsView.prototype = Herita
     if (functionItem) {
       let sourceUrl = functionItem.attachment.sourceUrl;
       let scriptOffset = functionItem.attachment.scriptOffset;
       let actualLocation = functionItem.attachment.actualLocation;
 
       DebuggerView.setEditorLocation(sourceUrl, actualLocation.start.line, {
         charOffset: scriptOffset,
         columnOffset: actualLocation.start.column,
+        align: "center",
         noDebug: true
       });
     }
   },
 
   _searchTimeout: null,
   _searchFunction: null,
   _searchedToken: ""
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -452,17 +452,18 @@ let DebuggerView = {
         aLine += this.editor.getPosition(aFlags.charOffset).line;
       }
 
       if (aFlags.lineOffset) {
         aLine += aFlags.lineOffset;
       }
 
       if (!aFlags.noCaret) {
-        this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 });
+        this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 },
+                              aFlags.align);
       }
 
       if (!aFlags.noDebug) {
         this.editor.setDebugLocation(aLine - 1);
       }
     }).then(null, console.error);
   },
 
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -32,16 +32,17 @@ browser.jar:
     content/browser/devtools/codemirror/codemirror.js                  (sourceeditor/codemirror/codemirror.js)
     content/browser/devtools/codemirror/codemirror.css                 (sourceeditor/codemirror/codemirror.css)
     content/browser/devtools/codemirror/javascript.js                  (sourceeditor/codemirror/javascript.js)
     content/browser/devtools/codemirror/xml.js                         (sourceeditor/codemirror/xml.js)
     content/browser/devtools/codemirror/css.js                         (sourceeditor/codemirror/css.js)
     content/browser/devtools/codemirror/htmlmixed.js                   (sourceeditor/codemirror/htmlmixed.js)
     content/browser/devtools/codemirror/activeline.js                  (sourceeditor/codemirror/activeline.js)
     content/browser/devtools/codemirror/matchbrackets.js               (sourceeditor/codemirror/matchbrackets.js)
+    content/browser/devtools/codemirror/closebrackets.js               (sourceeditor/codemirror/closebrackets.js)
     content/browser/devtools/codemirror/comment.js                     (sourceeditor/codemirror/comment.js)
     content/browser/devtools/codemirror/searchcursor.js                (sourceeditor/codemirror/search/searchcursor.js)
     content/browser/devtools/codemirror/search.js                      (sourceeditor/codemirror/search/search.js)
     content/browser/devtools/codemirror/dialog.js                      (sourceeditor/codemirror/dialog/dialog.js)
     content/browser/devtools/codemirror/dialog.css                     (sourceeditor/codemirror/dialog/dialog.css)
     content/browser/devtools/codemirror/mozilla.css                    (sourceeditor/codemirror/mozilla.css)
 *   content/browser/devtools/source-editor-overlay.xul                 (sourceeditor/source-editor-overlay.xul)
     content/browser/devtools/debugger.xul                              (debugger/debugger.xul)
--- a/browser/devtools/sourceeditor/codemirror/README
+++ b/browser/devtools/sourceeditor/codemirror/README
@@ -35,16 +35,17 @@ in the LICENSE file:
 
  * codemirror.css
  * codemirror.js
  * comment.js
  * dialog/dialog.css
  * dialog/dialog.js
  * javascript.js
  * matchbrackets.js
+ * closebrackets.js
  * search/match-highlighter.js
  * search/search.js
  * search/searchcursor.js
  * test/codemirror.html
  * test/cm_comment_test.js
  * test/cm_driver.js
  * test/cm_mode_javascript_test.js
  * test/cm_mode_test.css
@@ -52,9 +53,9 @@ in the LICENSE file:
  * test/cm_test.js
 
 # Footnotes
 
 [1] http://codemirror.net
 [2] browser/devtools/sourceeditor/codemirror
 [3] browser/devtools/sourceeditor/test/browser_codemirror.js
 [4] browser/devtools/jar.mn
-[5] browser/devtools/sourceeditor/editor.js
\ No newline at end of file
+[5] browser/devtools/sourceeditor/editor.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/codemirror/closebrackets.js
@@ -0,0 +1,82 @@
+(function() {
+  var DEFAULT_BRACKETS = "()[]{}''\"\"";
+  var DEFAULT_EXPLODE_ON_ENTER = "[]{}";
+  var SPACE_CHAR_REGEX = /\s/;
+
+  CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+    if (old != CodeMirror.Init && old)
+      cm.removeKeyMap("autoCloseBrackets");
+    if (!val) return;
+    var pairs = DEFAULT_BRACKETS, explode = DEFAULT_EXPLODE_ON_ENTER;
+    if (typeof val == "string") pairs = val;
+    else if (typeof val == "object") {
+      if (val.pairs != null) pairs = val.pairs;
+      if (val.explode != null) explode = val.explode;
+    }
+    var map = buildKeymap(pairs);
+    if (explode) map.Enter = buildExplodeHandler(explode);
+    cm.addKeyMap(map);
+  });
+
+  function charsAround(cm, pos) {
+    var str = cm.getRange(CodeMirror.Pos(pos.line, pos.ch - 1),
+                          CodeMirror.Pos(pos.line, pos.ch + 1));
+    return str.length == 2 ? str : null;
+  }
+
+  function buildKeymap(pairs) {
+    var map = {
+      name : "autoCloseBrackets",
+      Backspace: function(cm) {
+        if (cm.somethingSelected()) return CodeMirror.Pass;
+        var cur = cm.getCursor(), around = charsAround(cm, cur);
+        if (around && pairs.indexOf(around) % 2 == 0)
+          cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1));
+        else
+          return CodeMirror.Pass;
+      }
+    };
+    var closingBrackets = "";
+    for (var i = 0; i < pairs.length; i += 2) (function(left, right) {
+      if (left != right) closingBrackets += right;
+      function surround(cm) {
+        var selection = cm.getSelection();
+        cm.replaceSelection(left + selection + right);
+      }
+      function maybeOverwrite(cm) {
+        var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1));
+        if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass;
+        else cm.execCommand("goCharRight");
+      }
+      map["'" + left + "'"] = function(cm) {
+        if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment")
+          return CodeMirror.Pass;
+        if (cm.somethingSelected()) return surround(cm);
+        if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return;
+        var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1);
+        var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : "";
+        if (left == right && CodeMirror.isWordChar(curChar))
+          return CodeMirror.Pass;
+        if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar))
+          cm.replaceSelection(left + right, {head: ahead, anchor: ahead});
+        else
+          return CodeMirror.Pass;
+      };
+      if (left != right) map["'" + right + "'"] = maybeOverwrite;
+    })(pairs.charAt(i), pairs.charAt(i + 1));
+    return map;
+  }
+
+  function buildExplodeHandler(pairs) {
+    return function(cm) {
+      var cur = cm.getCursor(), around = charsAround(cm, cur);
+      if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+      cm.operation(function() {
+        var newPos = CodeMirror.Pos(cur.line + 1, 0);
+        cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input");
+        cm.indentLine(cur.line + 1, null, true);
+        cm.indentLine(cur.line + 2, null, true);
+      });
+    };
+  }
+})();
--- a/browser/devtools/sourceeditor/debugger.js
+++ b/browser/devtools/sourceeditor/debugger.js
@@ -57,46 +57,49 @@ function getSearchCursor(cm, query, pos)
     typeof query == "string" && query == query.toLowerCase());
 }
 
 /**
  * If there's a saved search, selects the next results.
  * Otherwise, creates a new search and selects the first
  * result.
  */
-function doSearch(cm, rev, query) {
+function doSearch(ctx, rev, query) {
+  let { cm } = ctx;
   let state = getSearchState(cm);
 
   if (state.query)
-    return searchNext(cm, rev);
+    return searchNext(ctx, rev);
 
   cm.operation(function () {
     if (state.query) return;
 
     state.query = query;
     state.posFrom = state.posTo = { line: 0, ch: 0 };
-    searchNext(cm, rev);
+    searchNext(ctx, rev);
   });
 }
 
 /**
  * Selects the next result of a saved search.
  */
-function searchNext(cm, rev) {
+function searchNext(ctx, rev) {
+  let { cm, ed } = ctx;
   cm.operation(function () {
     let state = getSearchState(cm)
     let cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
 
     if (!cursor.find(rev)) {
       cursor = getSearchCursor(cm, state.query, rev ?
         { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 });
       if (!cursor.find(rev))
         return;
     }
 
+    ed.alignLine(cursor.from().line, "center");
     cm.setSelection(cursor.from(), cursor.to());
     state.posFrom = cursor.from();
     state.posTo = cursor.to();
   });
 }
 
 /**
  * Clears the currently saved search.
@@ -231,37 +234,34 @@ function clearDebugLocation(ctx) {
     meta.debugLocation = null;
   }
 }
 
 /**
  * Starts a new search.
  */
 function find(ctx, query) {
-  let { cm } = ctx;
-  clearSearch(cm);
-  doSearch(cm, false, query);
+  clearSearch(ctx.cm);
+  doSearch(ctx, false, query);
 }
 
 /**
  * Finds the next item based on the currently saved search.
  */
 function findNext(ctx, query) {
-  let { cm } = ctx;
-  doSearch(cm, false, query);
+  doSearch(ctx, false, query);
 }
 
 /**
  * Finds the previous item based on the currently saved search.
  */
 function findPrev(ctx, query) {
-  let { cm } = ctx;
-  doSearch(cm, true, query);
+  doSearch(ctx, true, query);
 }
 
 
 // Export functions
 
 [
   initialize, hasBreakpoint, addBreakpoint, removeBreakpoint,
   getBreakpoints, setDebugLocation, getDebugLocation,
   clearDebugLocation, find, findNext, findPrev
-].forEach(function (func) { module.exports[func.name] = func; });
\ No newline at end of file
+].forEach(function (func) { module.exports[func.name] = func; });
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -7,16 +7,20 @@
 
 const { Cu, Cc, Ci, components } = require("chrome");
 
 const TAB_SIZE    = "devtools.editor.tabsize";
 const EXPAND_TAB  = "devtools.editor.expandtab";
 const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
 const XUL_NS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+// Maximum allowed margin (in number of lines) from top or bottom of the editor
+// while shifting to a line which was initially out of view.
+const MAX_VERTICAL_OFFSET = 3;
+
 const promise = require("sdk/core/promise");
 const events  = require("devtools/shared/event-emitter");
 
 Cu.import("resource://gre/modules/Services.jsm");
 const L10N = Services.strings.createBundle(L10N_BUNDLE);
 
 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
 // JavaScript and CSS that is injected into an iframe in
@@ -29,16 +33,17 @@ const CM_STYLES   = [
 ];
 
 const CM_SCRIPTS  = [
   "chrome://browser/content/devtools/codemirror/codemirror.js",
   "chrome://browser/content/devtools/codemirror/dialog.js",
   "chrome://browser/content/devtools/codemirror/searchcursor.js",
   "chrome://browser/content/devtools/codemirror/search.js",
   "chrome://browser/content/devtools/codemirror/matchbrackets.js",
+  "chrome://browser/content/devtools/codemirror/closebrackets.js",
   "chrome://browser/content/devtools/codemirror/comment.js",
   "chrome://browser/content/devtools/codemirror/javascript.js",
   "chrome://browser/content/devtools/codemirror/xml.js",
   "chrome://browser/content/devtools/codemirror/css.js",
   "chrome://browser/content/devtools/codemirror/htmlmixed.js",
   "chrome://browser/content/devtools/codemirror/activeline.js"
 ];
 
@@ -54,17 +59,16 @@ const CM_IFRAME   =
 [ "    <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
   "  </head>" +
   "  <body></body>" +
   "</html>";
 
 const CM_MAPPING = [
   "focus",
   "hasFocus",
-  "setCursor",
   "getCursor",
   "somethingSelected",
   "setSelection",
   "getSelection",
   "replaceSelection",
   "undo",
   "redo",
   "clearHistory",
@@ -73,16 +77,18 @@ const CM_MAPPING = [
   "lineCount"
 ];
 
 const CM_JUMP_DIALOG = [
   L10N.GetStringFromName("gotoLineCmd.promptTitle")
     + " <input type=text style='width: 10em'/>"
 ];
 
+const { cssProperties, cssValues, cssColors } = getCSSKeywords();
+
 const editors = new WeakMap();
 
 Editor.modes = {
   text: { name: "text" },
   js:   { name: "javascript" },
   html: { name: "htmlmixed" },
   css:  { name: "css" }
 };
@@ -187,17 +193,31 @@ Editor.prototype = {
       // and its dependencies into its DOM.
 
       env.removeEventListener("load", onLoad, true);
       let win = env.contentWindow.wrappedJSObject;
 
       CM_SCRIPTS.forEach((url) =>
         Services.scriptloader.loadSubScript(url, win, "utf8"));
 
-      // Create a CodeMirror instance add support for context menus and
+      // Replace the propertyKeywords, colorKeywords and valueKeywords
+      // properties of the CSS MIME type with the values provided by Gecko.
+      let cssSpec = win.CodeMirror.resolveMode("text/css");
+      cssSpec.propertyKeywords = cssProperties;
+      cssSpec.colorKeywords = cssColors;
+      cssSpec.valueKeywords = cssValues;
+      win.CodeMirror.defineMIME("text/css", cssSpec);
+
+      let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
+      scssSpec.propertyKeywords = cssProperties;
+      scssSpec.colorKeywords = cssColors;
+      scssSpec.valueKeywords = cssValues;
+      win.CodeMirror.defineMIME("text/x-scss", scssSpec);
+
+      // 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);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
         this.emit("contextMenu");
         this.showContextMenu(doc, ev.screenX, ev.screenY);
@@ -429,16 +449,77 @@ Editor.prototype = {
 
       if (name === "initialize")
         return void funcs[name](ctx);
 
       this[name] = funcs[name].bind(null, ctx);
     });
   },
 
+  /**
+   * Gets the first visible line number in the editor.
+   */
+  getFirstVisibleLine: function () {
+    let cm = editors.get(this);
+    return cm.lineAtHeight(0, "local");
+  },
+
+  /**
+   * Scrolls the view such that the given line number is the first visible line.
+   */
+  setFirstVisibleLine: function (line) {
+    let cm = editors.get(this);
+    let { top } = cm.charCoords({line: line, ch: 0}, "local");
+    cm.scrollTo(0, top);
+  },
+
+  /**
+   * Sets the cursor to the specified {line, ch} position with an additional
+   * option to align the line at the "top", "center" or "bottom" of the editor
+   * with "top" being default value.
+   */
+  setCursor: function ({line, ch}, align) {
+    let cm = editors.get(this);
+    this.alignLine(line, align);
+    cm.setCursor({line: line, ch: ch});
+  },
+
+  /**
+   * Aligns the provided line to either "top", "center" or "bottom" of the
+   * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
+   * bottom.
+   */
+  alignLine: function(line, align) {
+    let cm = editors.get(this);
+    let from = cm.lineAtHeight(0, "page");
+    let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
+    let linesVisible = to - from;
+    let halfVisible = Math.round(linesVisible/2);
+
+    // If the target line is in view, skip the vertical alignment part.
+    if (line <= to && line >= from) {
+      return;
+    }
+
+    // Setting the offset so that the line always falls in the upper half
+    // of visible lines (lower half for bottom aligned).
+    // MAX_VERTICAL_OFFSET is the maximum allowed value.
+    let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
+
+    let topLine = {
+      "center": Math.max(line - halfVisible, 0),
+      "bottom": Math.max(line - linesVisible + offset, 0),
+      "top": Math.max(line - offset, 0)
+    }[align || "top"] || offset;
+
+    // Bringing down the topLine to total lines in the editor if exceeding.
+    topLine = Math.min(topLine, this.lineCount());
+    this.setFirstVisibleLine(topLine);
+  },
+
   destroy: function () {
     this.container = null;
     this.config = null;
     this.version = null;
     this.emit("destroy");
   }
 };
 
@@ -447,16 +528,54 @@ Editor.prototype = {
 
 CM_MAPPING.forEach(function (name) {
   Editor.prototype[name] = function (...args) {
     let cm = editors.get(this);
     return cm[name].apply(cm, args);
   };
 });
 
+// Since Gecko already provide complete and up to date list of CSS property
+// names, values and color names, we compute them so that they can replace
+// the ones used in CodeMirror while initiating an editor object. This is done
+// here instead of the file codemirror/css.js so as to leave that file untouched
+// and easily upgradable.
+function getCSSKeywords() {
+  function keySet(array) {
+    var keys = {};
+    for (var i = 0; i < array.length; ++i) {
+      keys[array[i]] = true;
+    }
+    return keys;
+  }
+
+  let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+                   .getService(Ci.inIDOMUtils);
+  let cssProperties = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
+  let cssColors = {};
+  let cssValues = {};
+  cssProperties.forEach(property => {
+    if (property.contains("color")) {
+      domUtils.getCSSValuesForProperty(property).forEach(value => {
+        cssColors[value] = true;
+      });
+    }
+    else {
+      domUtils.getCSSValuesForProperty(property).forEach(value => {
+        cssValues[value] = true;
+      });
+    }
+  });
+  return {
+    cssProperties: keySet(cssProperties),
+    cssValues: cssValues,
+    cssColors: cssColors
+  };
+}
+
 /**
  * Returns a controller object that can be used for
  * editor-specific commands such as find, jump to line,
  * copy/paste, etc.
  */
 function controller(ed, view) {
   return {
     supportsCommand: function (cmd) {
@@ -525,9 +644,9 @@ function controller(ed, view) {
       if (cmd == "cmd_gotoLine")
         ed.jumpToLine(cm);
     },
 
     onEvent: function () {}
   };
 }
 
-module.exports = Editor;
\ No newline at end of file
+module.exports = Editor;
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -68,17 +68,17 @@ StyleEditorUI.prototype = {
    * @return boolean
    */
   get isDirty()
   {
     if (this._markedDirty === true) {
       return true;
     }
     return this.editors.some((editor) => {
-      return editor.sourceEditor && editor.sourceEditor.dirty;
+      return editor.sourceEditor && !editor.sourceEditor.isClean();
     });
   },
 
   /*
    * Mark the style editor as having or not having unsaved changes.
    */
   set isDirty(value) {
     this._markedDirty = value;
@@ -146,18 +146,18 @@ StyleEditorUI.prototype = {
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
     // remember selected sheet and line number for next load
     if (this.selectedEditor && this.selectedEditor.sourceEditor) {
       let href = this.selectedEditor.styleSheet.href;
-      let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
-      this.selectStyleSheet(href, line, col);
+      let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
+      this.selectStyleSheet(href, line, ch);
     }
 
     this._clearStyleSheetEditors();
     this._view.removeAll();
 
     this.selectedEditor = null;
 
     this._root.classList.add("loading");
@@ -360,17 +360,17 @@ StyleEditorUI.prototype = {
    * @param  {number} col
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
     line = line || 0;
     col = col || 0;
 
     editor.getSourceEditor().then(() => {
-      editor.sourceEditor.setCaretPosition(line, col);
+      editor.sourceEditor.setCursor({line: line, ch: col});
     });
 
     this._view.activeSummary = editor.summary;
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -6,31 +6,36 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
-let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const Editor  = require("devtools/sourceeditor/editor");
+const promise = require("sdk/core/promise");
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
-Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 
-
 const SAVE_ERROR = "error-save";
 
 // max update frequency in ms (avoid potential typing lag and/or flicker)
 // @see StyleEditor.updateStylesheet
 const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
 
+function ctrl(k) {
+  return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
+}
+
 /**
  * StyleSheetEditor controls the editor linked to a particular StyleSheet
  * object.
  *
  * Emits events:
  *   'source-load': The source of the stylesheet has been fetched
  *   'property-change': A property on the underlying stylesheet has changed
  *   'source-editor-load': The source editor for this editor has been loaded
@@ -53,17 +58,20 @@ function StyleSheetEditor(styleSheet, wi
   this._window = win;
   this._isNew = isNew;
   this.savedFile = file;
 
   this.errorMessage = null;
 
   this._state = {   // state to use when inputElement attaches
     text: "",
-    selection: {start: 0, end: 0},
+    selection: {
+      start: {line: 0, ch: 0},
+      end: {line: 0, ch: 0}
+    },
     readOnly: false,
     topIndex: 0,              // the first visible line
   };
 
   this._styleSheetFilePath = null;
   if (styleSheet.href &&
       Services.io.extractScheme(this.styleSheet.href) == "file") {
     this._styleSheetFilePath = this.styleSheet.href;
@@ -87,17 +95,17 @@ StyleSheetEditor.prototype = {
   get sourceEditor() {
     return this._sourceEditor;
   },
 
   /**
    * Whether there are unsaved changes in the editor
    */
   get unsaved() {
-    return this._sourceEditor && this._sourceEditor.dirty;
+    return this._sourceEditor && !this._sourceEditor.isClean();
   },
 
   /**
    * Whether the editor is for a stylesheet created by the user
    * through the style editor UI.
    */
   get isNew() {
     return this._isNew;
@@ -195,63 +203,61 @@ StyleSheetEditor.prototype = {
   /**
    * Create source editor and load state into it.
    * @param  {DOMElement} inputElement
    *         Element to load source editor in
    */
   load: function(inputElement) {
     this._inputElement = inputElement;
 
-    let sourceEditor = new SourceEditor();
     let config = {
-      initialText: this._state.text,
-      showLineNumbers: true,
-      mode: SourceEditor.MODES.CSS,
+      value: this._state.text,
+      lineNumbers: true,
+      mode: Editor.modes.css,
       readOnly: this._state.readOnly,
-      keys: this._getKeyBindings()
+      autoCloseBrackets: "{}()[]",
+      extraKeys: this._getKeyBindings()
     };
+    let sourceEditor = new Editor(config);
 
-    sourceEditor.init(inputElement, config, function onSourceEditorReady() {
-      setupBracketCompletion(sourceEditor);
-      sourceEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
-                                    function onTextChanged(event) {
+    sourceEditor.appendTo(inputElement).then(() => {
+      sourceEditor.on("change", () => {
         this.updateStyleSheet();
-      }.bind(this));
+      });
 
       this._sourceEditor = sourceEditor;
 
       if (this._focusOnSourceEditorReady) {
         this._focusOnSourceEditorReady = false;
         sourceEditor.focus();
       }
 
-      sourceEditor.setTopIndex(this._state.topIndex);
+      sourceEditor.setFirstVisibleLine(this._state.topIndex);
       sourceEditor.setSelection(this._state.selection.start,
                                 this._state.selection.end);
 
       this.emit("source-editor-load");
-    }.bind(this));
+    });
 
-    sourceEditor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
-                                  this._onPropertyChange);
+    sourceEditor.on("change", this._onPropertyChange);
   },
 
   /**
    * Get the source editor for this editor.
    *
    * @return {Promise}
    *         Promise that will resolve with the editor.
    */
   getSourceEditor: function() {
     let deferred = promise.defer();
 
     if (this.sourceEditor) {
       return promise.resolve(this);
     }
-    this.on("source-editor-load", (event) => {
+    this.on("source-editor-load", () => {
       deferred.resolve(this);
     });
     return deferred.promise;
   },
 
   /**
    * Focus the Style Editor input.
    */
@@ -263,17 +269,17 @@ StyleSheetEditor.prototype = {
     }
   },
 
   /**
    * Event handler for when the editor is shown.
    */
   onShow: function() {
     if (this._sourceEditor) {
-      this._sourceEditor.setTopIndex(this._state.topIndex);
+      this._sourceEditor.setFirstVisibleLine(this._state.topIndex);
     }
     this.focus();
   },
 
   /**
    * Toggled the disabled state of the underlying stylesheet.
    */
   toggleDisabled: function() {
@@ -365,52 +371,39 @@ StyleSheetEditor.prototype = {
         FileUtils.closeSafeFileOutputStream(ostream);
         // remember filename for next save if any
         this._friendlyName = null;
         this.savedFile = returnFile;
 
         if (callback) {
           callback(returnFile);
         }
-        this.sourceEditor.dirty = false;
+        this.sourceEditor.markClean();
       }.bind(this));
     };
 
     showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile);
   },
 
   /**
     * Retrieve custom key bindings objects as expected by SourceEditor.
     * SourceEditor action names are not displayed to the user.
     *
     * @return {array} key binding objects for the source editor
     */
   _getKeyBindings: function() {
-    let bindings = [];
+    let bindings = {};
 
-    bindings.push({
-      action: "StyleEditor.save",
-      code: _("saveStyleSheet.commandkey"),
-      accel: true,
-      callback: function save() {
-        this.saveToFile(this.savedFile);
-        return true;
-      }.bind(this)
-    });
+    bindings[ctrl(_("saveStyleSheet.commandkey"))] = () => {
+      this.saveToFile(this.savedFile);
+    };
 
-    bindings.push({
-      action: "StyleEditor.saveAs",
-      code: _("saveStyleSheet.commandkey"),
-      accel: true,
-      shift: true,
-      callback: function saveAs() {
-        this.saveToFile();
-        return true;
-      }.bind(this)
-    });
+    bindings["Shift-" + ctrl(_("saveStyleSheet.commandkey"))] = () => {
+      this.saveToFile();
+    };
 
     return bindings;
   },
 
   /**
    * Clean up for this editor.
    */
   destroy: function() {
@@ -422,28 +415,16 @@ StyleSheetEditor.prototype = {
 
 
 const TAB_CHARS = "\t";
 
 const OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 const LINE_SEPARATOR = OS === "WINNT" ? "\r\n" : "\n";
 
 /**
-  * Return string that repeats text for aCount times.
-  *
-  * @param string text
-  * @param number aCount
-  * @return string
-  */
-function repeat(text, aCount)
-{
-  return (new Array(aCount + 1)).join(text);
-}
-
-/**
  * Prettify minified CSS text.
  * This prettifies CSS code where there is no indentation in usual places while
  * keeping original indentation as-is elsewhere.
  *
  * @param string text
  *        The CSS source to prettify.
  * @return string
  *         Prettified CSS source
@@ -464,17 +445,17 @@ function prettifyCSS(text)
 
     switch (c) {
       case "}":
         if (i - partStart > 1) {
           // there's more than just } on the line, add line
           parts.push(indent + text.substring(partStart, i));
           partStart = i;
         }
-        indent = repeat(TAB_CHARS, --indentLevel);
+        indent = TAB_CHARS.repeat(--indentLevel);
         /* fallthrough */
       case ";":
       case "{":
         shouldIndent = true;
         break;
     }
 
     if (shouldIndent) {
@@ -488,63 +469,14 @@ function prettifyCSS(text)
         }
         partStart = i + 1;
       } else {
         return text; // assume it is not minified, early exit
       }
     }
 
     if (c == "{") {
-      indent = repeat(TAB_CHARS, ++indentLevel);
+      indent = TAB_CHARS.repeat(++indentLevel);
     }
   }
   return parts.join(LINE_SEPARATOR);
 }
 
-
-/**
- * Set up bracket completion on a given SourceEditor.
- * This automatically closes the following CSS brackets: "{", "(", "["
- *
- * @param SourceEditor sourceEditor
- */
-function setupBracketCompletion(sourceEditor)
-{
-  let editorElement = sourceEditor.editorElement;
-  let pairs = {
-    123: { // {
-      closeString: "}",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
-    },
-    40: { // (
-      closeString: ")",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_0
-    },
-    91: { // [
-      closeString: "]",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
-    },
-  };
-
-  editorElement.addEventListener("keypress", function onKeyPress(event) {
-    let pair = pairs[event.charCode];
-    if (!pair || event.ctrlKey || event.metaKey ||
-        event.accelKey || event.altKey) {
-      return true;
-    }
-
-    // We detected an open bracket, sending closing character
-    let keyCode = pair.closeKeyCode;
-    let charCode = pair.closeString.charCodeAt(0);
-    let modifiers = 0;
-    let utils = editorElement.ownerDocument.defaultView.
-                  QueryInterface(Ci.nsIInterfaceRequestor).
-                  getInterface(Ci.nsIDOMWindowUtils);
-                  
-    if (utils.sendKeyEvent("keydown", keyCode, 0, modifiers)) {
-      utils.sendKeyEvent("keypress", 0, charCode, modifiers);
-    }
-    utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
-    // and rewind caret
-    sourceEditor.setCaretOffset(sourceEditor.getCaretOffset() - 1);
-  }, false);
-}
-
--- a/browser/devtools/styleeditor/test/browser_styleeditor_new.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.js
@@ -91,42 +91,34 @@ function testEditor(aEditor) {
   let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
   is(parseInt(ruleCount), 0,
      "new editor initially shows 0 rules");
 
   let computedStyle = content.getComputedStyle(content.document.body, null);
   is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
      "content's background color is initially white");
 
-  EventUtils.synthesizeKey("[", {accelKey: true}, gPanelWindow);
-  is(aEditor.sourceEditor.getText(), "",
-     "Nothing happened as it is a known shortcut in source editor");
-
-  EventUtils.synthesizeKey("]", {accelKey: true}, gPanelWindow);
-  is(aEditor.sourceEditor.getText(), "",
-     "Nothing happened as it is a known shortcut in source editor");
-
   for each (let c in TESTCASE_CSS_SOURCE) {
     EventUtils.synthesizeKey(c, {}, gPanelWindow);
   }
 
-  is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
-     "rule bracket has been auto-closed");
-
   ok(aEditor.unsaved,
      "new editor has unsaved flag");
 
   // we know that the testcase above will start a CSS transition
   content.addEventListener("transitionend", onTransitionEnd, false);
 }, gPanelWindow) ;
 }
 
 function onTransitionEnd() {
   content.removeEventListener("transitionend", onTransitionEnd, false);
 
+  is(gNewEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
+     "rule bracket has been auto-closed");
+
   let computedStyle = content.getComputedStyle(content.document.body, null);
   is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
      "content's background color has been updated to red");
 
   if (gNewEditor) {
     is(gNewEditor.styleSheet.href, gOriginalHref,
        "style sheet href did not change");
   }
--- a/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -49,19 +49,19 @@ function runTests()
   });
   gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
 }
 
 function testRemembered()
 {
   is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
 
-  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
   is(line, LINE_NO, "correct line selected");
-  is(col, COL_NO, "correct column selected");
+  is(ch, COL_NO, "correct column selected");
 
   testNewPage();
 }
 
 function testNewPage()
 {
   let count = 0;
   gUI.on("editor-added", function editorAdded(event, editor) {
@@ -75,25 +75,25 @@ function testNewPage()
   info("navigating to a different page");
   navigatePage();
 }
 
 function testNotRemembered()
 {
   is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
 
-  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
   is(line, 0, "first line is selected");
-  is(col, 0, "first column is selected");
+  is(ch, 0, "first column is selected");
 
   gUI = null;
   finish();
 }
 
 function reloadPage()
 {
   gContentWin.location.reload();
 }
 
 function navigatePage()
 {
   gContentWin.location = NEW_URI;
-}
\ No newline at end of file
+}
--- a/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
@@ -24,32 +24,34 @@ function test()
   content.location = TESTCASE_URI;
 }
 
 function runTests(aUI)
 {
   is(aUI.editors.length, 2,
      "there is 2 stylesheets initially");
 
-  aUI.editors[0].getSourceEditor().then(function onEditorAttached(aEditor) {
+  aUI.editors[0].getSourceEditor().then(aEditor => {
     executeSoon(function () {
       waitForFocus(function () {
         // queue a resize to inverse aspect ratio
         // this will trigger a detach and reattach (to workaround bug 254144)
         let originalSourceEditor = aEditor.sourceEditor;
-        aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
+        let editor = aEditor.sourceEditor;
+        editor.setCursor(editor.getPosition(4)); // to check the caret is preserved
 
         gOriginalWidth = gPanelWindow.outerWidth;
         gOriginalHeight = gPanelWindow.outerHeight;
         gPanelWindow.resizeTo(120, 480);
 
         executeSoon(function () {
           is(aEditor.sourceEditor, originalSourceEditor,
              "the editor still references the same SourceEditor instance");
-          is(aEditor.sourceEditor.getCaretOffset(), 4,
+          let editor = aEditor.sourceEditor;
+          is(editor.getOffset(editor.getCursor()), 4,
              "the caret position has been preserved");
 
           // queue a resize to original aspect ratio
           waitForFocus(function () {
             gPanelWindow.resizeTo(gOriginalWidth, gOriginalHeight);
             executeSoon(function () {
               finish();
             });
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -126,17 +126,17 @@ function checkStyleEditorForSheetAndLine
     failureFn: finishTest,
   });
 }
 
 function performLineCheck(aEditor, aLine, aCallback)
 {
   function checkForCorrectState()
   {
-    is(aEditor.sourceEditor.getCaretPosition().line, aLine,
+    is(aEditor.sourceEditor.getCursor().line, aLine,
        "correct line is selected");
     is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex,
        "correct stylesheet is selected in the editor");
 
     aCallback && executeSoon(aCallback);
   }
 
   waitForSuccess({