Bug 717369 - Autocomplete CSS properties and values in the Style Editor - Part 1 - No tests, r=anton, msucan, dcamp, robcee, harth
authorGirish Sharma <scrapmachines@gmail.com>
Tue, 28 Jan 2014 20:38:41 +0530
changeset 181582 fcbf8c877de97d3a9206e6e4a6a309f0af2c8f18
parent 181581 30d6d1f88b0028bd05868ad435677408a1b5d3a6
child 181583 025d4a4b4223575aaaf7442f416a064f000d6d79
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanton, msucan, dcamp, robcee, harth
bugs717369
milestone29.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 717369 - Autocomplete CSS properties and values in the Style Editor - Part 1 - No tests, r=anton, msucan, dcamp, robcee, harth
browser/app/profile/firefox.js
browser/devtools/framework/toolbox-options.xul
browser/devtools/inspector/selector-search.js
browser/devtools/inspector/test/browser_inspector_bug_674871.js
browser/devtools/inspector/test/browser_inspector_iframeTest.js
browser/devtools/sourceeditor/autocomplete.js
browser/devtools/sourceeditor/css-autocompleter.js
browser/devtools/sourceeditor/css-tokenizer.js
browser/devtools/sourceeditor/editor.js
browser/devtools/sourceeditor/moz.build
browser/devtools/styleeditor/StyleEditorUI.jsm
browser/devtools/styleeditor/StyleSheetEditor.jsm
browser/devtools/styleeditor/styleeditor-panel.js
browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
toolkit/devtools/server/actors/inspector.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1153,16 +1153,17 @@ pref("devtools.tilt.outro_transition", t
 // - enableCodeFolding: Whether to enable code folding or not.
 pref("devtools.scratchpad.recentFilesMax", 10);
 pref("devtools.scratchpad.showTrailingSpace", false);
 pref("devtools.scratchpad.enableCodeFolding", true);
 
 // Enable the Style Editor.
 pref("devtools.styleeditor.enabled", true);
 pref("devtools.styleeditor.source-maps-enabled", false);
+pref("devtools.styleeditor.autocompletion-enabled", true);
 
 // Enable the Shader Editor.
 pref("devtools.shadereditor.enabled", false);
 
 // Enable tools for Chrome development.
 pref("devtools.chrome.enabled", false);
 
 // Default theme ("dark" or "light")
--- a/browser/devtools/framework/toolbox-options.xul
+++ b/browser/devtools/framework/toolbox-options.xul
@@ -61,16 +61,19 @@
                     tooltiptext="&options.timestampMessages.tooltip;"
                     data-pref="devtools.webconsole.timestampMessages"/>
         </vbox>
         <label value="&options.styleeditor.label;"/>
         <vbox id="styleeditor-options" class="options-groupbox">
           <checkbox label="&options.stylesheetSourceMaps.label;"
                     tooltiptext="&options.stylesheetSourceMaps.tooltip;"
                     data-pref="devtools.styleeditor.source-maps-enabled"/>
+          <checkbox label="&options.stylesheetAutocompletion.label;"
+                    tooltiptext="&options.stylesheetAutocompletion.tooltip;"
+                    data-pref="devtools.styleeditor.autocompletion-enabled"/>
         </vbox>
         <label value="&options.profiler.label;"/>
         <vbox id="profiler-options" class="options-groupbox">
           <checkbox label="&options.showPlatformData.label;"
                     tooltiptext="&options.showPlatformData.tooltip;"
                     data-pref="devtools.profiler.ui.show-platform-data"/>
         </vbox>
         <label value="&options.context.advancedSettings;"/>
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -245,36 +245,33 @@ SelectorSearch.prototype = {
 
         if (!query.slice(-1).match(/[\.#\s>+]/)) {
           // Hide the popup if we have some matching nodes and the query is not
           // ending with [.# >] which means that the selector is not at the
           // beginning of a new class, tag or id.
           if (this.searchPopup.isOpen) {
             this.searchPopup.hidePopup();
           }
-        }
-        else {
-          this.showSuggestions();
+          this.searchBox.classList.remove("devtools-no-search-result");
+
+          return this._selectResult(0);
         }
-        this.searchBox.classList.remove("devtools-no-search-result");
-
-        return this._selectResult(0);
+        return this._selectResult(0).then(() => {
+          this.searchBox.classList.remove("devtools-no-search-result");
+        }).then( () => this.showSuggestions());
       }
-      else {
-        if (query.match(/[\s>+]$/)) {
-          this._lastValidSearch = query + "*";
-        }
-        else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
-          let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
-          this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
-        }
-        this.searchBox.classList.add("devtools-no-search-result");
-        this.showSuggestions();
+      if (query.match(/[\s>+]$/)) {
+        this._lastValidSearch = query + "*";
       }
-      return undefined;
+      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+        let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
+        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+      }
+      this.searchBox.classList.add("devtools-no-search-result");
+      return this.showSuggestions();
     });
   },
 
   /**
    * Handles keypresses inside the input box.
    */
   _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
     let query = this.searchBox.value;
@@ -409,23 +406,16 @@ SelectorSearch.prototype = {
     }
   },
 
   
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
   _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
-    // Sort alphabetically in increaseing order.
-    aList = aList.sort();
-    // Sort based on count= in decreasing order.
-    aList = aList.sort(function([a1,a2], [b1,b2]) {
-      return a2 < b2;
-    });
-
     let total = 0;
     let query = this.searchBox.value;
     let toLowerCase = false;
     let items = [];
     // In case of tagNames, change the case to small.
     if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
       toLowerCase = true;
     }
@@ -466,116 +456,49 @@ SelectorSearch.prototype = {
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function SelectorSearch_showSuggestions() {
-    if (!this.walker.isLocal()) {
-      return;
-    }
     let query = this.searchBox.value;
-    if (this._lastValidSearch != "" &&
-        this._lastToLastValidSearch != this._lastValidSearch) {
-      this._searchSuggestions = {
-        ids: new Map(),
-        classes: new Map(),
-        tags: new Map(),
-      };
-
-      let nodes = [];
-      try {
-        nodes = this.doc.querySelectorAll(this._lastValidSearch);
-      } catch (ex) {}
-      for (let node of nodes) {
-        this._searchSuggestions.ids.set(node.id, 1);
-        this._searchSuggestions.tags
-            .set(node.tagName,
-                 (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
-        for (let className of node.classList) {
-          this._searchSuggestions.classes
-            .set(className,
-                 (this._searchSuggestions.classes.get(className) || 0) + 1);
-        }
-      }
-      this._lastToLastValidSearch = this._lastValidSearch;
-    }
-    else if (this._lastToLastValidSearch != this._lastValidSearch) {
-      this._searchSuggestions = {
-        ids: new Map(),
-        classes: new Map(),
-        tags: new Map(),
-      };
-
-      if (query.length == 0) {
-        return;
-      }
-
-      let nodes = null;
-      if (this.state == this.States.CLASS) {
-        nodes = this.doc.querySelectorAll("[class]");
-        for (let node of nodes) {
-          for (let className of node.classList) {
-            this._searchSuggestions.classes
-              .set(className,
-                   (this._searchSuggestions.classes.get(className) || 0) + 1);
-          }
-        }
-      }
-      else if (this.state == this.States.ID) {
-        nodes = this.doc.querySelectorAll("[id]");
-        for (let node of nodes) {
-          this._searchSuggestions.ids.set(node.id, 1);
-        }
-      }
-      else if (this.state == this.States.TAG) {
-        nodes = this.doc.getElementsByTagName("*");
-        for (let node of nodes) {
-          this._searchSuggestions.tags
-              .set(node.tagName,
-                   (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
-        }
-      }
-      else {
-        return;
-      }
-      this._lastToLastValidSearch = this._lastValidSearch;
-    }
-
-    // Filter the suggestions based on search box value.
-    let result = [];
     let firstPart = "";
     if (this.state == this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
-      firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["",query])[1];
-      for (let [tag, count] of this._searchSuggestions.tags) {
-        if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) {
-          result.push([tag, count]);
-        }
-      }
+      firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
+      query = query.slice(0, query.length - firstPart.length);
     }
     else if (this.state == this.States.CLASS) {
       // gets the class that is being completed. For ex. '.foo.b' returns 'b'
       firstPart = query.match(/\.([^\.]*)$/)[1];
-      for (let [className, count] of this._searchSuggestions.classes) {
-        if (className.startsWith(firstPart)) {
-          result.push(["." + className, count]);
-        }
-      }
-      firstPart = "." + firstPart;
+      query = query.slice(0, query.length - firstPart.length - 1);
     }
     else if (this.state == this.States.ID) {
       // gets the id that is being completed. For ex. '.foo#b' returns 'b'
       firstPart = query.match(/#([^#]*)$/)[1];
-      for (let [id, count] of this._searchSuggestions.ids) {
-        if (id.startsWith(firstPart)) {
-          result.push(["#" + id, 1]);
-        }
+      query = query.slice(0, query.length - firstPart.length - 1);
+    }
+    // TODO: implement some caching so that over the wire request is not made
+    // everytime.
+    if (/[\s+>~]$/.test(query)) {
+      query += "*";
+    }
+    this._currentSuggesting = query;
+    return this.walker.getSuggestionsForQuery(query, firstPart, this.state).then(result => {
+      if (this._currentSuggesting != result.query) {
+        // This means that this response is for a previous request and the user
+        // as since typed something extra leading to a new request.
+        return;
       }
-      firstPart = "#" + firstPart;
-    }
-
-    this._showPopup(result, firstPart);
+      this._lastToLastValidSearch = this._lastValidSearch;
+      if (this.state == this.States.CLASS) {
+        firstPart = "." + firstPart;
+      }
+      else if (this.state == this.States.ID) {
+        firstPart = "#" + firstPart;
+      }
+      this._showPopup(result.suggestions, firstPart);
+    });
   },
 };
--- a/browser/devtools/inspector/test/browser_inspector_bug_674871.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_674871.js
@@ -63,17 +63,18 @@ function test()
     });
   }
 
   function isTheIframeHighlighted()
   {
     let outlineRect = getHighlighterOutlineRect();
     let iframeRect = iframeNode.getBoundingClientRect();
     for (let dim of ["width", "height", "top", "left"]) {
-      is(Math.floor(outlineRect[dim]), Math.floor(iframeRect[dim]), "Outline dimension is correct");
+      is(Math.floor(outlineRect[dim]), Math.floor(iframeRect[dim]),
+         "Outline dimension is correct " + outlineRect[dim]);
     }
 
     iframeNode.style.marginBottom = doc.defaultView.innerHeight + "px";
     doc.defaultView.scrollBy(0, 40);
 
     moveMouseOver(iframeNode, 40, 40, isTheIframeContentHighlighted);
   }
 
@@ -96,13 +97,13 @@ function test()
   {
     doc = inspector = iframeNode = iframeBodyNode = null;
     gBrowser.removeCurrentTab();
     finish();
   }
 
   function moveMouseOver(aElement, x, y, cb)
   {
+    inspector.toolbox.once("picker-node-hovered", cb);
     EventUtils.synthesizeMouse(aElement, x, y, {type: "mousemove"},
                                aElement.ownerDocument.defaultView);
-    inspector.toolbox.once("picker-node-hovered", cb);
   }
 }
--- a/browser/devtools/inspector/test/browser_inspector_iframeTest.js
+++ b/browser/devtools/inspector/test/browser_inspector_iframeTest.js
@@ -26,38 +26,37 @@ function createDocument() {
     iframe2 = iframe1.contentDocument.createElement('iframe');
 
     iframe2.addEventListener('load', function () {
       iframe2.removeEventListener("load", arguments.callee, false);
 
       div2 = iframe2.contentDocument.createElement('div');
       div2.textContent = 'nested div';
       iframe2.contentDocument.body.appendChild(div2);
-
       // Open the inspector, start the picker mode, and start the tests
       openInspector(aInspector => {
         inspector = aInspector;
         inspector.toolbox.startPicker().then(runTests);
       });
     }, false);
 
     iframe2.src = 'data:text/html,nested iframe';
     iframe1.contentDocument.body.appendChild(iframe2);
   }, false);
 
   iframe1.src = 'data:text/html,little iframe';
   doc.body.appendChild(iframe1);
 }
 
 function moveMouseOver(aElement, cb) {
-  EventUtils.synthesizeMouse(aElement, 2, 2, {type: "mousemove"},
-    aElement.ownerDocument.defaultView);
   inspector.toolbox.once("picker-node-hovered", () => {
     executeSoon(cb);
   });
+  EventUtils.synthesizeMouseAtCenter(aElement, {type: "mousemove"},
+    aElement.ownerDocument.defaultView);
 }
 
 function runTests() {
   testDiv1Highlighter();
 }
 
 function testDiv1Highlighter() {
   moveMouseOver(div1, () => {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -0,0 +1,166 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const cssAutoCompleter = require("devtools/sourceeditor/css-autocompleter");
+const { AutocompletePopup } = require("devtools/shared/autocomplete-popup");
+
+const privates = new WeakMap();
+
+/**
+ * Prepares an editor instance for autocompletion, setting up the popup and the
+ * CSS completer instance.
+ */
+function setupAutoCompletion(ctx, walker) {
+  let { cm, ed, Editor } = ctx;
+
+  let win = ed.container.contentWindow.wrappedJSObject;
+
+  let completer = null;
+  if (ed.config.mode == Editor.modes.css)
+    completer = new cssAutoCompleter({walker: walker});
+
+  let popup = new AutocompletePopup(win.parent.document, {
+    position: "after_start",
+    fixedWidth: true,
+    theme: "auto",
+    autoSelect: true
+  });
+
+  let keyMap = {
+    "Tab": cm => {
+      if (popup && popup.isOpen) {
+        cycleSuggestions(ed);
+        return;
+      }
+
+      return win.CodeMirror.Pass;
+    },
+    "Shift-Tab": cm => {
+      if (popup && popup.isOpen) {
+        cycleSuggestions(ed, true);
+        return;
+      }
+
+      return win.CodeMirror.Pass;
+    },
+  };
+  keyMap[Editor.accel("Space")] = cm => autoComplete(ctx);
+  cm.addKeyMap(keyMap);
+
+  cm.on("keydown", (cm, e) => onEditorKeypress(ed, e));
+  ed.on("change", () => autoComplete(ctx));
+  ed.on("destroy", () => {
+    cm.off("keydown", (cm, e) => onEditorKeypress(ed, e));
+    ed.off("change", () => autoComplete(ctx));
+    popup.destroy();
+    popup = null;
+    completer = null;
+  });
+
+  privates.set(ed, {
+    popup: popup,
+    completer: completer,
+    insertingSuggestion: false,
+    suggestionInsertedOnce: false
+  });
+}
+
+/**
+ * Provides suggestions to autocomplete the current token/word being typed.
+ */
+function autoComplete({ ed, cm }) {
+  let private = privates.get(ed);
+  let { completer, popup } = private;
+  if (!completer || private.insertingSuggestion || private.doNotAutocomplete) {
+    private.insertingSuggestion = false;
+    return;
+  }
+  let cur = ed.getCursor();
+  completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur)
+    .then(suggestions => {
+    if (!suggestions || !suggestions.length || !suggestions[0].preLabel) {
+      private.suggestionInsertedOnce = false;
+      popup.hidePopup();
+      return;
+    }
+    // The cursor is at the end of the currently entered part of the token, like
+    // "backgr|" but we need to open the popup at the beginning of the character
+    // "b". Thus we need to calculate the width of the entered part of the token
+    // ("backgr" here). 4 comes from the popup's left padding.
+    let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
+    popup.hidePopup();
+    popup.setItems(suggestions);
+    popup.openPopup(cm.display.cursor, -1 * left, 0);
+    private.suggestionInsertedOnce = false;
+  });
+}
+
+/**
+ * Cycles through provided suggestions by the popup in a top to bottom manner
+ * when `reverse` is not true. Opposite otherwise.
+ */
+function cycleSuggestions(ed, reverse) {
+  let private = privates.get(ed);
+  let { popup, completer } = private;
+  let cur = ed.getCursor();
+  private.insertingSuggestion = true;
+  if (!private.suggestionInsertedOnce) {
+    private.suggestionInsertedOnce = true;
+    let firstItem;
+    if (reverse) {
+      firstItem = popup.getItemAtIndex(popup.itemCount - 1);
+      popup.selectPreviousItem();
+    } else {
+      firstItem = popup.getItemAtIndex(0);
+      if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
+        firstItem = popup.getItemAtIndex(1);
+        popup.selectNextItem();
+      }
+    }
+    if (popup.itemCount == 1) {
+      popup.hidePopup();
+    }
+    ed.replaceText(firstItem.label.slice(firstItem.preLabel.length), cur, cur);
+  } else {
+    let fromCur = {
+      line: cur.line,
+      ch  : cur.ch - popup.selectedItem.label.length
+    };
+    if (reverse)
+      popup.selectPreviousItem();
+    else
+      popup.selectNextItem();
+    ed.replaceText(popup.selectedItem.label, fromCur, cur);
+  }
+}
+
+/**
+ * onkeydown handler for the editor instance to prevent autocompleting on some
+ * keypresses.
+ */
+function onEditorKeypress(ed, event) {
+  let private = privates.get(ed);
+  switch (event.keyCode) {
+    case event.DOM_VK_UP:
+    case event.DOM_VK_DOWN:
+    case event.DOM_VK_LEFT:
+    case event.DOM_VK_RIGHT:
+    case event.DOM_VK_BACK_SPACE:
+    case event.DOM_VK_DELETE:
+    case event.DOM_VK_ENTER:
+    case event.DOM_VK_RETURN:
+    case event.DOM_VK_ESCAPE:
+      private.doNotAutocomplete = true;
+      private.popup.hidePopup();
+      break;
+
+    default:
+      private.doNotAutocomplete = false;
+  }
+}
+
+// Export functions
+
+module.exports.setupAutoCompletion = setupAutoCompletion;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/css-autocompleter.js
@@ -0,0 +1,798 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Cc, Ci, Cu } = require('chrome');
+const cssTokenizer  = require("devtools/sourceeditor/css-tokenizer");
+const promise = Cu.import("resource://gre/modules/Promise.jsm");
+
+/**
+ * Here is what this file (+ ./css-tokenizer.js) do.
+ *
+ * The main objective here is to provide as much suggestions to the user editing
+ * a stylesheet in Style Editor. The possible things that can be suggested are:
+ *  - CSS property names
+ *  - CSS property values
+ *  - CSS Selectors
+ *  - Some other known CSS keywords
+ *
+ * Gecko provides a list of both property names and their corresponding values.
+ * We take out a list of matching selectors using the Inspector actor's
+ * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
+ * edited by the user, figure out what token or word is being written and last
+ * but the most difficult, what is being edited.
+ *
+ * The file 'css-tokenizer' helps in converting the CSS into meaningful tokens,
+ * each having a certain type associated with it. These tokens help us to figure
+ * out the currently edited word and to write a CSS state machine to figure out
+ * what the user is currently editing. By that, I mean, whether he is editing a
+ * selector or a property or a value, or even fine grained information like an
+ * id in the selector.
+ *
+ * The `resolveState` method iterated over the tokens spitted out by the
+ * tokenizer, using switch cases, follows a state machine logic and finally
+ * figures out these informations:
+ *  - The state of the CSS at the cursor (one out of CSS_STATES)
+ *  - The current token that is being edited `cmpleting`
+ *  - If the state is "selector", the selector state (one of SELECTOR_STATES)
+ *  - If the state is "selector", the current selector till the cursor
+ *  - If the state is "value", the corresponding property name
+ *
+ * In case of "value" and "property" states, we simply use the information
+ * provided by Gecko to filter out the possible suggestions.
+ * For "selector" state, we request the Inspector actor to query the page DOM
+ * and filter out the possible suggestions.
+ * For "media" and "keyframes" state, the only possible suggestions for now are
+ * "media" and "keyframes" respectively, although "media" can have suggestions
+ * like "max-width", "orientation" etc. Similarly "value" state can also have
+ * much better logical suggestions if we fine grain identify a sub state just
+ * like we do for the "selector" state.
+ */
+
+// Autocompletion types.
+
+const CSS_STATES = {
+  "null": "null",
+  property: "property",    // foo { bar|: … }
+  value: "value",          // foo {bar: baz|}
+  selector: "selector",    // f| {bar: baz}
+  media: "media",          // @med| , or , @media scr| { }
+  keyframes: "keyframes",  // @keyf|
+  frame: "frame",          // @keyframs foobar { t|
+};
+
+const SELECTOR_STATES = {
+  "null": "null",
+  id: "id",                // #f|
+  class: "class",          // #foo.b|
+  tag: "tag",              // fo|
+  pseudo: "pseudo",        // foo:|
+  attribute: "attribute",  // foo[b|
+  value: "value",          // foo[bar=b|
+};
+
+const { properties, propertyNames } = getCSSKeywords();
+
+/**
+ * Constructor for the autocompletion object.
+ *
+ * @param options {Object} An options object containing the following options:
+ *        - walker {Object} The object used for query selecting from the current
+ *                 target's DOM.
+ *        - maxEntries {Number} Maximum selectors suggestions to display.
+ */
+function CSSCompleter(options = {}) {
+  this.walker = options.walker;
+  this.maxEntries = options.maxEntries || 15;
+}
+
+CSSCompleter.prototype = {
+
+  /**
+   * Returns a list of suggestions based on the caret position.
+   *
+   * @param source {String} String of the source code.
+   * @param caret {Object} Cursor location with line and ch properties.
+   *
+   * @returns [{object}] A sorted list of objects containing the following
+   *          peroperties:
+   *          - label {String} Full keyword for the suggestion
+   *          - preLabel {String} Already entered part of the label
+   */
+  complete: function(source, caret) {
+    // Getting the context from the caret position.
+    if (!this.resolveState(source, caret)) {
+      // We couldn't resolve the context, we won't be able to complete.
+      return Promise.resolve([]);
+    }
+
+    // Properly suggest based on the state.
+    switch(this.state) {
+      case CSS_STATES.property:
+        return this.completeProperties(this.completing);
+
+      case CSS_STATES.value:
+        return this.completeValues(this.propertyName, this.completing);
+
+      case CSS_STATES.selector:
+        return this.suggestSelectors();
+
+      case CSS_STATES.media:
+      case CSS_STATES.keyframes:
+        if ("media".startsWith(this.completing)) {
+          return Promise.resolve([{
+            label: "media",
+            preLabel: this.completing
+          }]);
+        } else if ("keyframes".startsWith(this.completing)) {
+          return Promise.resolve([{
+            label: "keyrames",
+            preLabel: this.completing
+          }]);
+        }
+    }
+    return Promise.resolve([]);
+  },
+
+  /**
+   * Resolves the state of CSS at the cursor location. This method implements a
+   * custom written CSS state machine. The various switch statements provide the
+   * transition rules for the state. It also finds out various informatino about
+   * the nearby CSS like the property name being completed, the complete
+   * selector, etc.
+   *
+   * @param source {String} String of the source code.
+   * @param caret {Object} Cursor location with line and ch properties.
+   *
+   * @returns CSS_STATE
+   *          One of CSS_STATE enum or null if the state cannot be resolved.
+   */
+  resolveState: function(source, {line, ch}) {
+    // Function to return the last element of an array
+    let peek = arr => arr[arr.length - 1];
+    let tokens = cssTokenizer(source, {loc:true});
+    let tokIndex = tokens.length - 1;
+    if (tokens[tokIndex].loc.end.line < line ||
+       (tokens[tokIndex].loc.end.line === line &&
+        tokens[tokIndex].loc.end.column < ch)) {
+      // If the last token is not an EOF, we didn't tokenize it correctly.
+      // This special case is handled in case we couldn't tokenize, but the last
+      // token that *could be tokenized* was an identifier.
+      return null;
+    }
+    // Since last token is EOF, the cursor token is last - 1
+    tokIndex--;
+
+    // _state can be one of CSS_STATES;
+    let _state = CSS_STATES.null;
+    let cursor = 0;
+    // This will maintain a stack of paired elements like { & }, @m & }, : & ; etc
+    let scopeStack = [];
+    let token = null;
+    let propertyName = null;
+    let selector = "";
+    let selectorBeforeNot = "";
+    let selectorState = SELECTOR_STATES.null;
+    while (cursor <= tokIndex && (token = tokens[cursor++])) {
+      switch (_state) {
+        case CSS_STATES.property:
+          // From CSS_STATES.property, we can either go to CSS_STATES.value state
+          // when we hit the first ':' or CSS_STATES.selector if "}" is reached.
+          switch(token.tokenType) {
+            case ":":
+              scopeStack.push(":");
+              if (tokens[cursor - 2].tokenType != "WHITESPACE")
+                propertyName = tokens[cursor - 2].value;
+              else
+                propertyName = tokens[cursor - 3].value;
+              _state = CSS_STATES.value;
+              break;
+
+            case "}":
+              if (/[{f]/.test(peek(scopeStack))) {
+                let popped = scopeStack.pop();
+                if (popped == "f") {
+                  _state = CSS_STATES.frame;
+                } else {
+                  selector = "";
+                  _state = CSS_STATES.null;
+                }
+              }
+              break;
+          }
+          break;
+
+        case CSS_STATES.value:
+          // From CSS_STATES.value, we can go to one of CSS_STATES.property,
+          // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
+          switch(token.tokenType) {
+            case ";":
+              if (/[:]/.test(peek(scopeStack))) {
+                scopeStack.pop();
+                _state = CSS_STATES.property;
+              }
+              break;
+
+            case "}":
+              if (peek(scopeStack) == ":")
+                scopeStack.pop();
+
+              if (/[{f]/.test(peek(scopeStack))) {
+                let popped = scopeStack.pop();
+                if (popped == "f") {
+                  _state = CSS_STATES.frame;
+                } else {
+                  selector = "";
+                  _state = CSS_STATES.null;
+                }
+              }
+              break;
+          }
+          break;
+
+        case CSS_STATES.selector:
+          // From CSS_STATES.selector, we can only go to CSS_STATES.property when
+          // we hit "{"
+          if (token.tokenType == "{") {
+            scopeStack.push("{");
+            _state = CSS_STATES.property;
+            break;
+          }
+          switch(selectorState) {
+            case SELECTOR_STATES.id:
+            case SELECTOR_STATES.class:
+            case SELECTOR_STATES.tag:
+              switch(token.tokenType) {
+                case "HASH":
+                  selectorState = SELECTOR_STATES.id;
+                  selector += "#" + token.value;
+                  break;
+
+                case "DELIM":
+                  if (token.value == ".") {
+                    selectorState = SELECTOR_STATES.class;
+                    selector += ".";
+                    if (cursor <= tokIndex &&
+                        tokens[cursor].tokenType == "IDENT") {
+                      token = tokens[cursor++];
+                      selector += token.value;
+                    }
+                  } else if (token.value == "#") {
+                    selectorState = SELECTOR_STATES.id;
+                    selector += "#";
+                  } else if (/[>~+]/.test(token.value)) {
+                    selectorState = SELECTOR_STATES.null;
+                    selector += token.value;
+                  } else if (token.value == ",") {
+                    selectorState = SELECTOR_STATES.null;
+                    selector = "";
+                  }
+                  break;
+
+                case ":":
+                  selectorState = SELECTOR_STATES.pseudo;
+                  selector += ":";
+                  if (cursor > tokIndex)
+                    break;
+
+                  token = tokens[cursor++];
+                  switch(token.tokenType) {
+                    case "FUNCTION":
+                      selectorState = SELECTOR_STATES.null;
+                      selectorBeforeNot = selector;
+                      selector = "";
+                      scopeStack.push("(");
+                      break;
+
+                    case "IDENT":
+                      selector += token.value;
+                      break;
+                  }
+                  break;
+
+                case "[":
+                  selectorState = SELECTOR_STATES.attribute;
+                  scopeStack.push("[");
+                  selector += "[";
+                  break;
+
+                case ")":
+                  if (peek(scopeStack) == "(") {
+                    scopeStack.pop();
+                    selector = selectorBeforeNot + "not(" + selector + ")";
+                    selectorState = SELECTOR_STATES.null;
+                  }
+                  break;
+
+                case "WHITESPACE":
+                  selectorState = SELECTOR_STATES.null;
+                  selector && (selector += " ");
+                  break;
+              }
+              break;
+
+            case SELECTOR_STATES.null:
+              // From SELECTOR_STATES.null state, we can go to one of
+              // SELECTOR_STATES.id, SELECTOR_STATES.class or SELECTOR_STATES.tag
+              switch(token.tokenType) {
+                case "HASH":
+                  selectorState = SELECTOR_STATES.id;
+                  selector += "#" + token.value;
+                  break;
+
+                case "IDENT":
+                  selectorState = SELECTOR_STATES.tag;
+                  selector += token.value;
+                  break;
+
+                case "DELIM":
+                  if (token.value == ".") {
+                    selectorState = SELECTOR_STATES.class;
+                    selector += ".";
+                    if (cursor <= tokIndex &&
+                        tokens[cursor].tokenType == "IDENT") {
+                      token = tokens[cursor++];
+                      selector += token.value;
+                    }
+                  } else if (token.value == "#") {
+                    selectorState = SELECTOR_STATES.id;
+                    selector += "#";
+                  } else if (token.value == "*") {
+                    selectorState = SELECTOR_STATES.tag;
+                    selector += "*";
+                  } else if (/[>~+]/.test(token.value)) {
+                    selector += token.value;
+                  } else if (token.value == ",") {
+                    selectorState = SELECTOR_STATES.null;
+                    selector = "";
+                  }
+                  break;
+
+                case ":":
+                  selectorState = SELECTOR_STATES.pseudo;
+                  selector += ":";
+                  if (cursor > tokIndex)
+                    break;
+
+                  token = tokens[cursor++];
+                  switch(token.tokenType) {
+                    case "FUNCTION":
+                      selectorState = SELECTOR_STATES.null;
+                      selectorBeforeNot = selector;
+                      selector = "";
+                      scopeStack.push("(");
+                      break;
+
+                    case "IDENT":
+                      selector += token.value;
+                      break;
+                  }
+                  break;
+
+                case "[":
+                  selectorState = SELECTOR_STATES.attribute;
+                  scopeStack.push("[");
+                  selector += "[";
+                  break;
+
+                case ")":
+                  if (peek(scopeStack) == "(") {
+                    scopeStack.pop();
+                    selector = selectorBeforeNot + "not(" + selector + ")";
+                    selectorState = SELECTOR_STATES.null;
+                  }
+                  break;
+
+                case "WHITESPACE":
+                  selector && (selector += " ");
+                  break;
+              }
+              break;
+
+            case SELECTOR_STATES.pseudo:
+              switch(token.tokenType) {
+                case "DELIM":
+                  if (/[>~+]/.test(token.value)) {
+                    selectorState = SELECTOR_STATES.null;
+                    selector += token.value;
+                  } else if (token.value == ",") {
+                    selectorState = SELECTOR_STATES.null;
+                    selector = "";
+                  }
+                  break;
+
+                case ":":
+                  selectorState = SELECTOR_STATES.pseudo;
+                  selector += ":";
+                  if (cursor > tokIndex)
+                    break;
+
+                  token = tokens[cursor++];
+                  switch(token.tokenType) {
+                    case "FUNCTION":
+                      selectorState = SELECTOR_STATES.null;
+                      selectorBeforeNot = selector;
+                      selector = "";
+                      scopeStack.push("(");
+                      break;
+
+                    case "IDENT":
+                      selector += token.value;
+                      break;
+                  }
+                  break;
+
+                case "[":
+                  selectorState = SELECTOR_STATES.attribute;
+                  scopeStack.push("[");
+                  selector += "[";
+                  break;
+
+                case "WHITESPACE":
+                  selectorState = SELECTOR_STATES.null;
+                  selector && (selector += " ");
+                  break;
+              }
+              break;
+
+            case SELECTOR_STATES.attribute:
+              switch(token.tokenType) {
+                case "DELIM":
+                  if (/[~|^$*]/.test(token.value)) {
+                    selector += token.value;
+                    token = tokens[cursor++];
+                  }
+                  if(token.value == "=") {
+                    selectorState = SELECTOR_STATES.value;
+                    selector += token.value;
+                  }
+                  break;
+
+                case "IDENT":
+                case "STRING":
+                  selector += token.value;
+                  break;
+
+                case "]":
+                  if (peek(scopeStack) == "[")
+                    scopeStack.pop();
+
+                  selectorState = SELECTOR_STATES.null;
+                  selector += "]";
+                  break;
+
+                case "WHITESPACE":
+                  selector && (selector += " ");
+                  break;
+              }
+              break;
+
+            case SELECTOR_STATES.value:
+              switch(token.tokenType) {
+                case "STRING":
+                case "IDENT":
+                  selector += token.value;
+                  break;
+
+                case "]":
+                  if (peek(scopeStack) == "[")
+                    scopeStack.pop();
+
+                  selectorState = SELECTOR_STATES.null;
+                  selector += "]";
+                  break;
+
+                case "WHITESPACE":
+                  selector && (selector += " ");
+                  break;
+              }
+              break;
+          }
+          break;
+
+        case CSS_STATES.null:
+          // From CSS_STATES.null state, we can go to either CSS_STATES.media or
+          // CSS_STATES.selector.
+          switch(token.tokenType) {
+            case "HASH":
+              selectorState = SELECTOR_STATES.id;
+              selector = "#" + token.value;
+              _state = CSS_STATES.selector;
+              break;
+
+            case "IDENT":
+              selectorState = SELECTOR_STATES.tag;
+              selector = token.value;
+              _state = CSS_STATES.selector;
+              break;
+
+            case "DELIM":
+              if (token.value == ".") {
+                selectorState = SELECTOR_STATES.class;
+                selector = ".";
+                _state = CSS_STATES.selector;
+                if (cursor <= tokIndex &&
+                    tokens[cursor].tokenType == "IDENT") {
+                  token = tokens[cursor++];
+                  selector += token.value;
+                }
+              } else if (token.value == "#") {
+                selectorState = SELECTOR_STATES.id;
+                selector = "#";
+                _state = CSS_STATES.selector;
+              } else if (token.value == "*") {
+                selectorState = SELECTOR_STATES.tag;
+                selector = "*";
+                _state = CSS_STATES.selector;
+              }
+              break;
+
+            case ":":
+              _state = CSS_STATES.selector;
+              selectorState = SELECTOR_STATES.pseudo;
+              selector += ":";
+              if (cursor > tokIndex)
+                break;
+
+              token = tokens[cursor++];
+              switch(token.tokenType) {
+                case "FUNCTION":
+                  selectorState = SELECTOR_STATES.null;
+                  selectorBeforeNot = selector;
+                  selector = "";
+                  scopeStack.push("(");
+                  break;
+
+                case "IDENT":
+                  selector += token.value;
+                  break;
+              }
+              break;
+
+            case "[":
+              _state = CSS_STATES.selector;
+              selectorState = SELECTOR_STATES.attribute;
+              scopeStack.push("[");
+              selector += "[";
+              break;
+
+            case "AT-KEYWORD":
+              _state = token.value.startsWith("m") ? CSS_STATES.media
+                                                   : CSS_STATES.keyframes;
+              break;
+
+            case "}":
+              if (peek(scopeStack) == "@m")
+                scopeStack.pop();
+
+              break;
+          }
+          break;
+
+        case CSS_STATES.media:
+          // From CSS_STATES.media, we can only go to CSS_STATES.null state when
+          // we hit the first '{'
+          if (token.tokenType == "{") {
+            scopeStack.push("@m");
+            _state = CSS_STATES.null;
+          }
+          break;
+
+        case CSS_STATES.keyframes:
+          // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
+          // when we hit the first '{'
+          if (token.tokenType == "{") {
+            scopeStack.push("@k");
+            _state = CSS_STATES.frame;
+          }
+          break;
+
+        case CSS_STATES.frame:
+          // From CSS_STATES.frame, we can either go to CSS_STATES.property state
+          // when we hit the first '{' or to CSS_STATES.selector when we hit '}'
+          if (token.tokenType == "{") {
+            scopeStack.push("f");
+            _state = CSS_STATES.property;
+          } else if (token.tokenType == "}") {
+            if (peek(scopeStack) == "@k")
+              scopeStack.pop();
+
+            _state = CSS_STATES.null;
+          }
+          break;
+      }
+    }
+    this.state = _state;
+    if (!token)
+      return _state;
+
+    if (token && token.tokenType != "WHITESPACE") {
+      this.completing = ((token.value || token.repr || token.tokenType) + "")
+                          .slice(0, ch - token.loc.start.column)
+                          .replace(/^[.#]$/, "");
+    } else {
+      this.completing = "";
+    }
+    // Special check for !important; case.
+    if (tokens[cursor - 2] && tokens[cursor - 2].value == "!" &&
+        this.completing == "important".slice(0, this.completing.length)) {
+      this.completing = "!" + this.completing;
+    }
+    this.propertyName = _state == CSS_STATES.value ? propertyName : null;
+    selector = selector.slice(0, selector.length + token.loc.end.column - ch);
+    this.selector = _state == CSS_STATES.selector ? selector : null;
+    this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
+    return _state;
+  },
+
+  /**
+   * Queries the DOM Walker actor for suggestions regarding the selector being
+   * completed
+   */
+  suggestSelectors: function () {
+    let walker = this.walker;
+    if (!walker)
+      return Promise.resolve([]);
+
+    let query = this.selector;
+    // Even though the selector matched atleast one node, there is still
+    // possibility of suggestions.
+    switch(this.selectorState) {
+      case SELECTOR_STATES.null:
+        query += "*";
+        break;
+
+      case SELECTOR_STATES.tag:
+        query = query.slice(0, query.length - this.completing.length);
+        break;
+
+      case SELECTOR_STATES.id:
+      case SELECTOR_STATES.class:
+      case SELECTOR_STATES.pseudo:
+        if (/^[.:#]$/.test(this.completing)) {
+          query = query.slice(0, query.length - this.completing.length);
+          this.completing = "";
+        } else {
+          query = query.slice(0, query.length - this.completing.length - 1);
+        }
+        break;
+    }
+
+    if (/[\s+>~]$/.test(query) &&
+        this.selectorState != SELECTOR_STATES.attribute &&
+        this.selectorState != SELECTOR_STATES.value) {
+      query += "*";
+    }
+
+    // Set the values that this request was supposed to suggest to.
+    this._currentQuery = query;
+    return walker.getSuggestionsForQuery(query, this.completing, this.selectorState)
+                 .then(result => this.prepareSelectorResults(result));
+  },
+
+ /**
+  * Prepares the selector suggestions returned by the walker actor.
+  */
+  prepareSelectorResults: function(result) {
+    if (this._currentQuery != result.query)
+      return [];
+
+    result = result.suggestions;
+    let query = this.selector;
+    let completion = [];
+    for (let value of result) {
+      switch(this.selectorState) {
+        case SELECTOR_STATES.id:
+        case SELECTOR_STATES.class:
+        case SELECTOR_STATES.pseudo:
+          if (/^[.:#]$/.test(this.completing)) {
+            value[0] = query.slice(0, query.length - this.completing.length) +
+                       value[0];
+          } else {
+            value[0] = query.slice(0, query.length - this.completing.length - 1) +
+                       value[0];
+          }
+          break;
+
+        case SELECTOR_STATES.tag:
+          value[0] = query.slice(0, query.length - this.completing.length) +
+                     value[0];
+          break;
+
+        case SELECTOR_STATES.null:
+          value[0] = query + value[0];
+          break;
+
+        default:
+         value[0] = query.slice(0, query.length - this.completing.length) +
+                    value[0];
+      }
+      completion.push({
+        label: value[0],
+        preLabel: query,
+        score: value[1]
+      });
+      if (completion.length > this.maxEntries - 1)
+        break;
+    }
+    return completion;
+  },
+
+  /**
+   * Returns CSS property name suggestions based on the input.
+   *
+   * @param startProp {String} Initial part of the property being completed.
+   */
+  completeProperties: function(startProp) {
+    let finalList = [];
+    let length = propertyNames.length;
+    let i = 0, count = 0;
+    for (; i < length && count < this.maxEntries; i++) {
+      if (propertyNames[i].startsWith(startProp)) {
+        count++;
+        finalList.push({
+          preLabel: startProp,
+          label: propertyNames[i]
+        });
+      } else if (propertyNames[i] > startProp) {
+        // We have crossed all possible matches alphabetically.
+        break;
+      }
+    }
+    return Promise.resolve(finalList);
+  },
+
+  /**
+   * Returns CSS value suggestions based on the corresponding property.
+   *
+   * @param propName {String} The property to which the value being completed
+   *        belongs.
+   * @param startValue {String} Initial part of the value being completed.
+   */
+  completeValues: function(propName, startValue) {
+    let finalList = [];
+    let list = ["!important;", ...(properties[propName] || [])];
+    let length = list.length;
+    let i = 0, count = 0;
+    for (; i < length && count < this.maxEntries; i++) {
+      if (list[i].startsWith(startValue)) {
+        count++;
+        finalList.push({
+          preLabel: startValue,
+          label: list[i]
+        });
+      } else if (list[i] > startValue) {
+        // We have crossed all possible matches alphabetically.
+        break;
+      }
+    }
+    return Promise.resolve(finalList);
+  },
+}
+
+/**
+ * Returns a list of all property names and a map of property name vs possible
+ * CSS values provided by the Gecko engine.
+ *
+ * @return {Object} An object with following properties:
+ *         - propertyNames {Array} Array of string containing all the possible
+ *                         CSS property names.
+ *         - properties {Object|Map} A map where key is the property name and
+ *                      value is an array of string containing all the possible
+ *                      CSS values the property can have.
+ */
+function getCSSKeywords() {
+  let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+                   .getService(Ci.inIDOMUtils);
+  let props = {};
+  let propNames = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
+  propNames.forEach(prop => {
+    props[prop] = domUtils.getCSSValuesForProperty(prop).sort();
+  });
+  return {
+    properties: props,
+    propertyNames: propNames.sort()
+  };
+}
+
+module.exports = CSSCompleter;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/css-tokenizer.js
@@ -0,0 +1,717 @@
+/**
+ * This file is taken from the below mentioned url and is under CC0 license.
+ * https://github.com/tabatkins/css-parser/blob/master/tokenizer.js
+ * Please retain this comment while updating this file from upstream.
+ */
+
+(function (root, factory) {
+    // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
+    // Rhino, and plain browser loading.
+    if (typeof define === 'function' && define.amd) {
+        define(['exports'], factory);
+    } else if (typeof exports !== 'undefined') {
+        factory(exports);
+    } else {
+        factory(root);
+    }
+}(this, function (exports) {
+
+var between = function (num, first, last) { return num >= first && num <= last; }
+function digit(code) { return between(code, 0x30,0x39); }
+function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); }
+function uppercaseletter(code) { return between(code, 0x41,0x5a); }
+function lowercaseletter(code) { return between(code, 0x61,0x7a); }
+function letter(code) { return uppercaseletter(code) || lowercaseletter(code); }
+function nonascii(code) { return code >= 0xa0; }
+function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; }
+function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; }
+function nonprintable(code) { return between(code, 0,8) || between(code, 0xe,0x1f) || between(code, 0x7f,0x9f); }
+function newline(code) { return code == 0xa || code == 0xc; }
+function whitespace(code) { return newline(code) || code == 9 || code == 0x20; }
+function badescape(code) { return newline(code) || isNaN(code); }
+
+// Note: I'm not yet acting smart enough to actually handle astral characters.
+var maximumallowedcodepoint = 0x10ffff;
+
+function tokenize(str, options) {
+  if(options == undefined) options = {transformFunctionWhitespace:false, scientificNotation:false};
+  var i = -1;
+  var tokens = [];
+  var state = "data";
+  var code;
+  var currtoken;
+
+  // Line number information.
+  var line = 0;
+  var column = 0;
+  // The only use of lastLineLength is in reconsume().
+  var lastLineLength = 0;
+  var incrLineno = function() {
+    line += 1;
+    lastLineLength = column;
+    column = 0;
+  };
+  var locStart = {line:line, column:column};
+
+  var next = function(num) { if(num === undefined) num = 1; return str.charCodeAt(i+num); };
+  var consume = function(num) {
+    if(num === undefined)
+      num = 1;
+    i += num;
+    code = str.charCodeAt(i);
+    if (newline(code)) incrLineno();
+    else column += num;
+    //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
+    return true;
+  };
+  var reconsume = function() {
+    i -= 1;
+    if (newline(code)) {
+      line -= 1;
+      column = lastLineLength;
+    } else {
+      column -= 1;
+    }
+    locStart.line = line;
+    locStart.column = column;
+    return true;
+  };
+  var eof = function() { return i >= str.length; };
+  var donothing = function() {};
+  var emit = function(token) {
+    if(token) {
+      token.finish();
+    } else {
+      token = currtoken.finish();
+    }
+    if (options.loc === true) {
+      token.loc = {};
+      token.loc.start = {line:locStart.line, column:locStart.column};
+      locStart = {line: line, column: column};
+      token.loc.end = locStart;
+    }
+    tokens.push(token);
+    //console.log('Emitting ' + token);
+    currtoken = undefined;
+    return true;
+  };
+  var create = function(token) { currtoken = token; return true; };
+  var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + " in state " + state + ".");return true; };
+  var switchto = function(newstate) {
+    state = newstate;
+    //console.log('Switching to ' + state);
+    return true;
+  };
+  var consumeEscape = function() {
+    // Assume the the current character is the \
+    consume();
+    if(hexdigit(code)) {
+      // Consume 1-6 hex digits
+      var digits = [];
+      for(var total = 0; total < 6; total++) {
+        if(hexdigit(code)) {
+          digits.push(code);
+          consume();
+        } else { break; }
+      }
+      var value = parseInt(digits.map(String.fromCharCode).join(''), 16);
+      if( value > maximumallowedcodepoint ) value = 0xfffd;
+      // If the current char is whitespace, cool, we'll just eat it.
+      // Otherwise, put it back.
+      if(!whitespace(code)) reconsume();
+      return value;
+    } else {
+      return code;
+    }
+  };
+
+  for(;;) {
+    if(i > str.length*2) return "I'm infinite-looping!";
+    consume();
+    switch(state) {
+    case "data":
+      if(whitespace(code)) {
+        emit(new WhitespaceToken);
+        while(whitespace(next())) consume();
+      }
+      else if(code == 0x22) switchto("double-quote-string");
+      else if(code == 0x23) switchto("hash");
+      else if(code == 0x27) switchto("single-quote-string");
+      else if(code == 0x28) emit(new OpenParenToken);
+      else if(code == 0x29) emit(new CloseParenToken);
+      else if(code == 0x2b) {
+        if(digit(next()) || (next() == 0x2e && digit(next(2)))) switchto("number") && reconsume();
+        else emit(new DelimToken(code));
+      }
+      else if(code == 0x2d) {
+        if(next(1) == 0x2d && next(2) == 0x3e) consume(2) && emit(new CDCToken);
+        else if(digit(next()) || (next(1) == 0x2e && digit(next(2)))) switchto("number") && reconsume();
+        else if(namestartchar(next())) switchto("identifier") && reconsume();
+        else emit(new DelimToken(code));
+      }
+      else if(code == 0x2e) {
+        if(digit(next())) switchto("number") && reconsume();
+        else emit(new DelimToken(code));
+      }
+      else if(code == 0x2f) {
+        if(next() == 0x2a) switchto("comment");
+        else emit(new DelimToken(code));
+      }
+      else if(code == 0x3a) emit(new ColonToken);
+      else if(code == 0x3b) emit(new SemicolonToken);
+      else if(code == 0x3c) {
+        if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) consume(3) && emit(new CDOToken);
+        else emit(new DelimToken(code));
+      }
+      else if(code == 0x40) switchto("at-keyword");
+      else if(code == 0x5b) emit(new OpenSquareToken);
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new DelimToken(code));
+        else switchto("identifier") && reconsume();
+      }
+      else if(code == 0x5d) emit(new CloseSquareToken);
+      else if(code == 0x7b) emit(new OpenCurlyToken);
+      else if(code == 0x7d) emit(new CloseCurlyToken);
+      else if(digit(code)) switchto("number") && reconsume();
+      else if(code == 0x55 || code == 0x75) {
+        if(next(1) == 0x2b && hexdigit(next(2))) consume() && switchto("unicode-range");
+        else if((next(1) == 0x52 || next(1) == 0x72) && (next(2) == 0x4c || next(2) == 0x6c) && (next(3) == 0x28)) consume(3) && switchto("url");
+        else switchto("identifier") && reconsume();
+      }
+      else if(namestartchar(code)) switchto("identifier") && reconsume();
+      else if(eof()) { emit(new EOFToken); return tokens; }
+      else emit(new DelimToken(code));
+      break;
+
+    case "double-quote-string":
+      if(currtoken == undefined) create(new StringToken);
+
+      if(code == 0x22) emit() && switchto("data");
+      else if(eof()) parseerror() && emit() && switchto("data");
+      else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
+        else if(newline(next())) consume();
+        else currtoken.append(consumeEscape());
+      }
+      else currtoken.append(code);
+      break;
+
+    case "single-quote-string":
+      if(currtoken == undefined) create(new StringToken);
+
+      if(code == 0x27) emit() && switchto("data");
+      else if(eof()) parseerror() && emit() && switchto("data");
+      else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
+        else if(newline(next())) consume();
+        else currtoken.append(consumeEscape());
+      }
+      else currtoken.append(code);
+      break;
+
+    case "hash":
+      if(namechar(code)) create(new HashToken(code)) && switchto("hash-rest");
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
+        else create(new HashToken(consumeEscape())) && switchto('hash-rest');
+      }
+      else emit(new DelimToken(0x23)) && switchto('data') && reconsume();
+      break;
+
+    case "hash-rest":
+      if(namechar(code)) currtoken.append(code);
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "comment":
+      if(code == 0x2a) {
+        if(next() == 0x2f) consume() && switchto('data');
+        else donothing();
+      }
+      else if(eof()) parseerror() && switchto('data') && reconsume();
+      else donothing();
+      break;
+
+    case "at-keyword":
+      if(code == 0x2d) {
+        if(namestartchar(next())) consume() && create(new AtKeywordToken([0x40,code])) && switchto('at-keyword-rest');
+        else emit(new DelimToken(0x40)) && switchto('data') && reconsume();
+      }
+      else if(namestartchar(code)) create(new AtKeywordToken(code)) && switchto('at-keyword-rest');
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
+        else create(new AtKeywordToken(consumeEscape())) && switchto('at-keyword-rest');
+      }
+      else emit(new DelimToken(0x40)) && switchto('data') && reconsume();
+      break;
+
+    case "at-keyword-rest":
+      if(namechar(code)) currtoken.append(code);
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "identifier":
+      if(code == 0x2d) {
+        if(namestartchar(next())) create(new IdentifierToken(code)) && switchto('identifier-rest');
+        else switchto('data') && reconsume();
+      }
+      else if(namestartchar(code)) create(new IdentifierToken(code)) && switchto('identifier-rest');
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && switchto("data") && reconsume();
+        else create(new IdentifierToken(consumeEscape())) && switchto('identifier-rest');
+      }
+      else switchto('data') && reconsume();
+      break;
+
+    case "identifier-rest":
+      if(namechar(code)) currtoken.append(code);
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data');
+      else if(whitespace(code) && options.transformFunctionWhitespace) switchto('transform-function-whitespace');
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "transform-function-whitespace":
+      if(whitespace(code)) donothing();
+      else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data');
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "number":
+      create(new NumberToken());
+
+      if(code == 0x2d) {
+        if(digit(next())) consume() && currtoken.append([0x2d,code]) && switchto('number-rest');
+        else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2d,0x2e,code]) && switchto('number-fraction');
+        else switchto('data') && reconsume();
+      }
+      else if(code == 0x2b) {
+        if(digit(next())) consume() && currtoken.append([0x2b,code]) && switchto('number-rest');
+        else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2b,0x2e,code]) && switchto('number-fraction');
+        else switchto('data') && reconsume();
+      }
+      else if(digit(code)) currtoken.append(code) && switchto('number-rest');
+      else if(code == 0x2e) {
+        if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
+        else switchto('data') && reconsume();
+      }
+      else switchto('data') && reconsume();
+      break;
+
+    case "number-rest":
+      if(digit(code)) currtoken.append(code);
+      else if(code == 0x2e) {
+        if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
+        else emit() && switchto('data') && reconsume();
+      }
+      else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume();
+      else if(code == 0x45 || code == 0x65) {
+        if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension');
+        else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation');
+        else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation');
+        else create(new DimensionToken(currtoken,code)) && switchto('dimension');
+      }
+      else if(code == 0x2d) {
+        if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
+        else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
+        else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
+        else emit() && switchto('data') && reconsume();
+      }
+      else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
+      else if(code == 0x5c) {
+        if(badescape(next)) emit() && switchto('data') && reconsume();
+        else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension');
+      }
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "number-fraction":
+      currtoken.type = "number";
+
+      if(digit(code)) currtoken.append(code);
+      else if(code == 0x2e) emit() && switchto('data') && reconsume();
+      else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data') && reconsume();
+      else if(code == 0x45 || code == 0x65) {
+        if(!options.scientificNotation) create(new DimensionToken(currtoken,code)) && switchto('dimension');
+        else if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation');
+        else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation');
+        else create(new DimensionToken(currtoken,code)) && switchto('dimension');
+      }
+      else if(code == 0x2d) {
+        if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
+        else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
+        else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
+        else emit() && switchto('data') && reconsume();
+      }
+      else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
+      else if(code == 0x5c) {
+        if(badescape(next)) emit() && switchto('data') && reconsume();
+        else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension');
+      }
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "dimension":
+      if(namechar(code)) currtoken.append(code);
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && emit() && switchto('data') && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "sci-notation":
+      if(digit(code)) currtoken.append(code);
+      else emit() && switchto('data') && reconsume();
+      break;
+
+    case "url":
+      if(code == 0x22) switchto('url-double-quote');
+      else if(code == 0x27) switchto('url-single-quote');
+      else if(code == 0x29) emit(new URLToken) && switchto('data');
+      else if(whitespace(code)) donothing();
+      else switchto('url-unquoted') && reconsume();
+      break;
+
+    case "url-double-quote":
+      if(currtoken == undefined) create(new URLToken);
+
+      if(code == 0x22) switchto('url-end');
+      else if(newline(code)) parseerror() && switchto('bad-url');
+      else if(code == 0x5c) {
+        if(newline(next())) consume();
+        else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else currtoken.append(code);
+      break;
+
+    case "url-single-quote":
+      if(currtoken == undefined) create(new URLToken);
+
+      if(code == 0x27) switchto('url-end');
+      else if(newline(code)) parseerror() && switchto('bad-url');
+      else if(code == 0x5c) {
+        if(newline(next())) consume();
+        else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
+        else currtoken.append(consumeEscape());
+      }
+      else currtoken.append(code);
+      break;
+
+    case "url-end":
+      if(whitespace(code)) donothing();
+      else if(code == 0x29) emit() && switchto('data');
+      else parseerror() && switchto('bad-url') && reconsume();
+      break;
+
+    case "url-unquoted":
+      if(currtoken == undefined) create(new URLToken);
+
+      if(whitespace(code)) switchto('url-end');
+      else if(code == 0x29) emit() && switchto('data');
+      else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) parseerror() && switchto('bad-url');
+      else if(code == 0x5c) {
+        if(badescape(next())) parseerror() && switchto('bad-url');
+        else currtoken.append(consumeEscape());
+      }
+      else currtoken.append(code);
+      break;
+
+    case "bad-url":
+      if(code == 0x29) emit(new BadURLToken) && switchto('data');
+      else if(code == 0x5c) {
+        if(badescape(next())) donothing();
+        else consumeEscape()
+      }
+      else donothing();
+      break;
+
+    case "unicode-range":
+      // We already know that the current code is a hexdigit.
+
+      var start = [code], end = [code];
+
+      for(var total = 1; total < 6; total++) {
+        if(hexdigit(next())) {
+          consume();
+          start.push(code);
+          end.push(code);
+        }
+        else break;
+      }
+
+      if(next() == 0x3f) {
+        for(;total < 6; total++) {
+          if(next() == 0x3f) {
+            consume();
+            start.push("0".charCodeAt(0));
+            end.push("f".charCodeAt(0));
+          }
+          else break;
+        }
+        emit(new UnicodeRangeToken(start,end)) && switchto('data');
+      }
+      else if(next(1) == 0x2d && hexdigit(next(2))) {
+        consume();
+        consume();
+        end = [code];
+        for(var total = 1; total < 6; total++) {
+          if(hexdigit(next())) {
+            consume();
+            end.push(code);
+          }
+          else break;
+        }
+        emit(new UnicodeRangeToken(start,end)) && switchto('data');
+      }
+      else emit(new UnicodeRangeToken(start)) && switchto('data');
+      break;
+
+    default:
+      console.log("Unknown state '" + state + "'");
+    }
+  }
+}
+
+function stringFromCodeArray(arr) {
+  return String.fromCharCode.apply(null,arr.filter(function(e){return e;}));
+}
+
+function CSSParserToken(options) { return this; }
+CSSParserToken.prototype.finish = function() { return this; }
+CSSParserToken.prototype.toString = function() { return this.tokenType; }
+CSSParserToken.prototype.toJSON = function() { return this.toString(); }
+
+function BadStringToken() { return this; }
+BadStringToken.prototype = new CSSParserToken;
+BadStringToken.prototype.tokenType = "BADSTRING";
+
+function BadURLToken() { return this; }
+BadURLToken.prototype = new CSSParserToken;
+BadURLToken.prototype.tokenType = "BADURL";
+
+function WhitespaceToken() { return this; }
+WhitespaceToken.prototype = new CSSParserToken;
+WhitespaceToken.prototype.tokenType = "WHITESPACE";
+WhitespaceToken.prototype.toString = function() { return "WS"; }
+
+function CDOToken() { return this; }
+CDOToken.prototype = new CSSParserToken;
+CDOToken.prototype.tokenType = "CDO";
+
+function CDCToken() { return this; }
+CDCToken.prototype = new CSSParserToken;
+CDCToken.prototype.tokenType = "CDC";
+
+function ColonToken() { return this; }
+ColonToken.prototype = new CSSParserToken;
+ColonToken.prototype.tokenType = ":";
+
+function SemicolonToken() { return this; }
+SemicolonToken.prototype = new CSSParserToken;
+SemicolonToken.prototype.tokenType = ";";
+
+function OpenCurlyToken() { return this; }
+OpenCurlyToken.prototype = new CSSParserToken;
+OpenCurlyToken.prototype.tokenType = "{";
+
+function CloseCurlyToken() { return this; }
+CloseCurlyToken.prototype = new CSSParserToken;
+CloseCurlyToken.prototype.tokenType = "}";
+
+function OpenSquareToken() { return this; }
+OpenSquareToken.prototype = new CSSParserToken;
+OpenSquareToken.prototype.tokenType = "[";
+
+function CloseSquareToken() { return this; }
+CloseSquareToken.prototype = new CSSParserToken;
+CloseSquareToken.prototype.tokenType = "]";
+
+function OpenParenToken() { return this; }
+OpenParenToken.prototype = new CSSParserToken;
+OpenParenToken.prototype.tokenType = "(";
+
+function CloseParenToken() { return this; }
+CloseParenToken.prototype = new CSSParserToken;
+CloseParenToken.prototype.tokenType = ")";
+
+function EOFToken() { return this; }
+EOFToken.prototype = new CSSParserToken;
+EOFToken.prototype.tokenType = "EOF";
+
+function DelimToken(code) {
+  this.value = String.fromCharCode(code);
+  return this;
+}
+DelimToken.prototype = new CSSParserToken;
+DelimToken.prototype.tokenType = "DELIM";
+DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; }
+
+function StringValuedToken() { return this; }
+StringValuedToken.prototype = new CSSParserToken;
+StringValuedToken.prototype.append = function(val) {
+  if(val instanceof Array) {
+    for(var i = 0; i < val.length; i++) {
+      this.value.push(val[i]);
+    }
+  } else {
+    this.value.push(val);
+  }
+  return true;
+}
+StringValuedToken.prototype.finish = function() {
+  this.value = stringFromCodeArray(this.value);
+  return this;
+}
+
+function IdentifierToken(val) {
+  this.value = [];
+  this.append(val);
+}
+IdentifierToken.prototype = new StringValuedToken;
+IdentifierToken.prototype.tokenType = "IDENT";
+IdentifierToken.prototype.toString = function() { return "IDENT("+this.value+")"; }
+
+function FunctionToken(val) {
+  // These are always constructed by passing an IdentifierToken
+  this.value = val.finish().value;
+}
+FunctionToken.prototype = new CSSParserToken;
+FunctionToken.prototype.tokenType = "FUNCTION";
+FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; }
+
+function AtKeywordToken(val) {
+  this.value = [];
+  this.append(val);
+}
+AtKeywordToken.prototype = new StringValuedToken;
+AtKeywordToken.prototype.tokenType = "AT-KEYWORD";
+AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; }
+
+function HashToken(val) {
+  this.value = [];
+  this.append(val);
+}
+HashToken.prototype = new StringValuedToken;
+HashToken.prototype.tokenType = "HASH";
+HashToken.prototype.toString = function() { return "HASH("+this.value+")"; }
+
+function StringToken(val) {
+  this.value = [];
+  this.append(val);
+}
+StringToken.prototype = new StringValuedToken;
+StringToken.prototype.tokenType = "STRING";
+StringToken.prototype.toString = function() { return "\""+this.value+"\""; }
+
+function URLToken(val) {
+  this.value = [];
+  this.append(val);
+}
+URLToken.prototype = new StringValuedToken;
+URLToken.prototype.tokenType = "URL";
+URLToken.prototype.toString = function() { return "URL("+this.value+")"; }
+
+function NumberToken(val) {
+  this.value = [];
+  this.append(val);
+  this.type = "integer";
+}
+NumberToken.prototype = new StringValuedToken;
+NumberToken.prototype.tokenType = "NUMBER";
+NumberToken.prototype.toString = function() {
+  if(this.type == "integer")
+    return "INT("+this.value+")";
+  return "NUMBER("+this.value+")";
+}
+NumberToken.prototype.finish = function() {
+  this.repr = stringFromCodeArray(this.value);
+  this.value = this.repr * 1;
+  if(Math.abs(this.value) % 1 != 0) this.type = "number";
+  return this;
+}
+
+function PercentageToken(val) {
+  // These are always created by passing a NumberToken as val
+  val.finish();
+  this.value = val.value;
+  this.repr = val.repr;
+}
+PercentageToken.prototype = new CSSParserToken;
+PercentageToken.prototype.tokenType = "PERCENTAGE";
+PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; }
+
+function DimensionToken(val,unit) {
+  // These are always created by passing a NumberToken as the val
+  val.finish();
+  this.num = val.value;
+  this.unit = [];
+  this.repr = val.repr;
+  this.append(unit);
+}
+DimensionToken.prototype = new CSSParserToken;
+DimensionToken.prototype.tokenType = "DIMENSION";
+DimensionToken.prototype.toString = function() { return "DIM("+this.num+","+this.unit+")"; }
+DimensionToken.prototype.append = function(val) {
+  if(val instanceof Array) {
+    for(var i = 0; i < val.length; i++) {
+      this.unit.push(val[i]);
+    }
+  } else {
+    this.unit.push(val);
+  }
+  return true;
+}
+DimensionToken.prototype.finish = function() {
+  this.unit = stringFromCodeArray(this.unit);
+  this.repr += this.unit;
+  return this;
+}
+
+function UnicodeRangeToken(start,end) {
+  // start and end are array of char codes, completely finished
+  start = parseInt(stringFromCodeArray(start),16);
+  if(end === undefined) end = start + 1;
+  else end = parseInt(stringFromCodeArray(end),16);
+
+  if(start > maximumallowedcodepoint) end = start;
+  if(end < start) end = start;
+  if(end > maximumallowedcodepoint) end = maximumallowedcodepoint;
+
+  this.start = start;
+  this.end = end;
+  return this;
+}
+UnicodeRangeToken.prototype = new CSSParserToken;
+UnicodeRangeToken.prototype.tokenType = "UNICODE-RANGE";
+UnicodeRangeToken.prototype.toString = function() {
+  if(this.start+1 == this.end)
+    return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+")";
+  if(this.start < this.end)
+    return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+"-"+this.end.toString(16).toUpperCase()+")";
+  return "UNICODE-RANGE()";
+}
+UnicodeRangeToken.prototype.contains = function(code) {
+  return code >= this.start && code < this.end;
+}
+
+
+// Exportation.
+// TODO: also export the various tokens objects?
+module.exports = tokenize;
+
+}));
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -765,17 +765,17 @@ Editor.prototype = {
    * }
    *
    * editor.extend({ hello: hello });
    * editor.hello('Mozilla');
    */
   extend: function (funcs) {
     Object.keys(funcs).forEach((name) => {
       let cm  = editors.get(this);
-      let ctx = { ed: this, cm: cm };
+      let ctx = { ed: this, cm: cm, Editor: Editor};
 
       if (name === "initialize") {
         funcs[name](ctx);
         return;
       }
 
       this[name] = funcs[name].bind(null, ctx);
     });
--- a/browser/devtools/sourceeditor/moz.build
+++ b/browser/devtools/sourceeditor/moz.build
@@ -4,12 +4,15 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 TEST_DIRS += ['test']
 
 JS_MODULES_PATH = 'modules/devtools/sourceeditor'
 
 EXTRA_JS_MODULES += [
+    'autocomplete.js',
+    'css-autocompleter.js',
+    'css-tokenizer.js',
     'debugger.js',
     'editor.js'
 ]
 
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -11,16 +11,17 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 Cu.import("resource:///modules/devtools/SplitView.jsm");
 Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
 
 const LOAD_ERROR = "error-load";
@@ -55,37 +56,27 @@ function StyleEditorUI(debuggee, target,
   this.selectedEditor = null;
 
   this._updateSourcesLabel = this._updateSourcesLabel.bind(this);
   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
   this._onNewDocument = this._onNewDocument.bind(this);
   this._clear = this._clear.bind(this);
   this._onError = this._onError.bind(this);
 
-  this.createUI();
-
-  this._debuggee.getStyleSheets().then((styleSheets) => {
-    this._resetStyleSheetList(styleSheets);
-
-    this._target.on("will-navigate", this._clear);
-    this._target.on("navigate", this._onNewDocument);
-  });
-
   this._prefObserver = new PrefObserver("devtools.styleeditor.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument);
 }
 
 StyleEditorUI.prototype = {
   /**
    * Get whether any of the editors have unsaved changes.
    *
    * @return boolean
    */
-  get isDirty()
-  {
+  get isDirty() {
     if (this._markedDirty === true) {
       return true;
     }
     return this.editors.some((editor) => {
       return editor.sourceEditor && !editor.sourceEditor.isClean();
     });
   },
 
@@ -100,16 +91,34 @@ StyleEditorUI.prototype = {
    * Index of selected stylesheet in document.styleSheets
    */
   get selectedStyleSheetIndex() {
     return this.selectedEditor ?
            this.selectedEditor.styleSheet.styleSheetIndex : -1;
   },
 
   /**
+   * Initiates the style editor ui creation and the inspector front to get
+   * reference to the walker.
+   */
+  initialize: function() {
+    let toolbox = gDevTools.getToolbox(this._target);
+    return toolbox.initInspector().then(() => {
+      this._walker = toolbox.walker;
+    }).then(() => this.createUI())
+      .then(() => this._debuggee.getStyleSheets())
+      .then((styleSheets) => {
+      this._resetStyleSheetList(styleSheets);
+
+      this._target.on("will-navigate", this._clear);
+      this._target.on("navigate", this._onNewDocument);
+    });
+  },
+
+  /**
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
 
     this._view = new SplitView(viewRoot);
 
     wire(this._view.rootElement, ".style-editor-newButton", function onNew() {
@@ -231,17 +240,18 @@ StyleEditorUI.prototype = {
    * @param {StyleSheet}  styleSheet
    *        Object representing stylesheet
    * @param {nsIfile}  file
    *         Optional file object that sheet was imported from
    * @param {Boolean} isNew
    *         Optional if stylesheet is a new sheet created by user
    */
   _addStyleSheetEditor: function(styleSheet, file, isNew) {
-    let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew);
+    let editor =
+      new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker);
 
     editor.on("property-change", this._summaryChange.bind(this, editor));
     editor.on("style-applied", this._summaryChange.bind(this, editor));
     editor.on("error", this._onError);
 
     this.editors.push(editor);
 
     editor.fetchSource(this._sourceLoaded.bind(this, editor));
@@ -253,18 +263,17 @@ StyleEditorUI.prototype = {
    * new stylesheet on the debuggee for it.
    *
    * @param {mixed} file
    *        Optional nsIFile or filename string.
    *        If not set a file picker will be shown.
    * @param {nsIWindow} parentWindow
    *        Optional parent window for the file picker.
    */
-  _importFromFile: function(file, parentWindow)
-  {
+  _importFromFile: function(file, parentWindow) {
     let onFileSelected = function(file) {
       if (!file) {
         // nothing selected
         return;
       }
       NetUtil.asyncFetch(file, (stream, status) => {
         if (!Components.isSuccessCode(status)) {
           this.emit("error", LOAD_ERROR);
@@ -405,18 +414,19 @@ StyleEditorUI.prototype = {
           this._selectEditor(editor);
         }
 
         if (this._styleSheetToSelect
             && this._styleSheetToSelect.href == editor.styleSheet.href) {
           this.switchToSelectedSheet();
         }
 
-        // If this is the first stylesheet, select it
-        if (!this.selectedEditor
+        // If this is the first stylesheet and there is no pending request to
+        // select a particular style sheet, select this sheet.
+        if (!this.selectedEditor && !this._styleSheetBoundToSelect
             && editor.styleSheet.styleSheetIndex == 0) {
           this._selectEditor(editor);
         }
 
         this.emit("editor-added", editor);
       }.bind(this),
 
       onShow: function(summary, details, data) {
@@ -438,16 +448,21 @@ StyleEditorUI.prototype = {
   /**
    * Switch to the editor that has been marked to be selected.
    */
   switchToSelectedSheet: function() {
     let sheet = this._styleSheetToSelect;
 
     for each (let editor in this.editors) {
       if (editor.styleSheet.href == sheet.href) {
+        // The _styleSheetBoundToSelect will always hold the latest pending
+        // requested style sheet (with line and column) which is not yet
+        // selected by the source editor. Only after we select that particular
+        // editor and go the required line and column, it will become null.
+        this._styleSheetBoundToSelect = this._styleSheetToSelect;
         this._selectEditor(editor, sheet.line, sheet.col);
         this._styleSheetToSelect = null;
         return;
       }
     }
   },
 
   /**
@@ -461,16 +476,17 @@ StyleEditorUI.prototype = {
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
     line = line || 0;
     col = col || 0;
 
     editor.getSourceEditor().then(() => {
       editor.sourceEditor.setCursor({line: line, ch: col});
+      this._styleSheetBoundToSelect = null;
     });
 
     this.getEditorSummary(editor).then((summary) => {
       this._view.activeSummary = summary;
     })
   },
 
   getEditorSummary: function(editor) {
@@ -499,18 +515,17 @@ StyleEditorUI.prototype = {
    *        and the editor is not initialized we focus the first stylesheet. If
    *        a stylesheet is not passed and the editor is initialized we ignore
    *        the call.
    * @param {Number} [line]
    *        Line to which the caret should be moved (zero-indexed).
    * @param {Number} [col]
    *        Column to which the caret should be moved (zero-indexed).
    */
-  selectStyleSheet: function(href, line, col)
-  {
+  selectStyleSheet: function(href, line, col) {
     this._styleSheetToSelect = {
       href: href,
       line: line,
       col: col,
     };
 
     /* Switch to the editor for this sheet, if it exists yet.
        Otherwise each editor will be checked when it's created. */
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -10,31 +10,35 @@ this.EXPORTED_SYMBOLS = ["StyleSheetEdit
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const Editor  = require("devtools/sourceeditor/editor");
 const promise = require("sdk/core/promise");
 const {CssLogic} = require("devtools/styleinspector/css-logic");
+const AutoCompleter = require("devtools/sourceeditor/autocomplete");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 
 const LOAD_ERROR = "error-load";
 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;
 
+// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
+const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
+
 /**
  * StyleSheetEditor controls the editor linked to a particular StyleSheet
  * object.
  *
  * Emits events:
  *   'property-change': A property on the underlying stylesheet has changed
  *   'source-editor-load': The source editor for this editor has been loaded
  *   'error': An error has occured
@@ -42,26 +46,29 @@ const UPDATE_STYLESHEET_THROTTLE_DELAY =
  * @param {StyleSheet|OriginalSource}  styleSheet
  *        Stylesheet or original source to show
  * @param {DOMWindow}  win
  *        panel window for style editor
  * @param {nsIFile}  file
  *        Optional file that the sheet was imported from
  * @param {boolean} isNew
  *        Optional whether the sheet was created by the user
+ * @param {Walker} walker
+ *        Optional walker used for selectors autocompletion
  */
-function StyleSheetEditor(styleSheet, win, file, isNew) {
+function StyleSheetEditor(styleSheet, win, file, isNew, walker) {
   EventEmitter.decorate(this);
 
   this.styleSheet = styleSheet;
   this._inputElement = null;
   this._sourceEditor = null;
   this._window = win;
   this._isNew = isNew;
   this.savedFile = file;
+  this.walker = walker;
 
   this.errorMessage = null;
 
   let readOnly = false;
   if (styleSheet.isOriginalSource) {
     // live-preview won't work with sources that need compilation
     readOnly = true;
   }
@@ -192,16 +199,20 @@ StyleSheetEditor.prototype = {
       readOnly: this._state.readOnly,
       autoCloseBrackets: "{}()[]",
       extraKeys: this._getKeyBindings(),
       contextMenu: "sourceEditorContextMenu"
     };
     let sourceEditor = new Editor(config);
 
     sourceEditor.appendTo(inputElement).then(() => {
+      if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) {
+        sourceEditor.extend(AutoCompleter);
+        sourceEditor.setupAutoCompletion(this.walker);
+      }
       sourceEditor.on("save", () => {
         this.saveToFile();
       });
 
       sourceEditor.on("change", () => {
         this.updateStyleSheet();
       });
 
@@ -391,16 +402,19 @@ StyleSheetEditor.prototype = {
 
     return bindings;
   },
 
   /**
    * Clean up for this editor.
    */
   destroy: function() {
+    if (this.sourceEditor) {
+      this.sourceEditor.destroy();
+    }
     this.styleSheet.off("property-change", this._onPropertyChange);
     this.styleSheet.off("error", this._onError);
   }
 }
 
 
 const TAB_CHARS = "\t";
 
--- a/browser/devtools/styleeditor/styleeditor-panel.js
+++ b/browser/devtools/styleeditor/styleeditor-panel.js
@@ -60,21 +60,23 @@ StyleEditorPanel.prototype = {
       if (this.target.form.styleSheetsActor) {
         this._debuggee = StyleSheetsFront(this.target.client, this.target.form);
       }
       else {
         /* We're talking to a pre-Firefox 29 server-side */
         this._debuggee = StyleEditorFront(this.target.client, this.target.form);
       }
       this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc);
-      this.UI.on("error", this._showError);
+      this.UI.initialize().then(() => {
+        this.UI.on("error", this._showError);
 
-      this.isReady = true;
+        this.isReady = true;
 
-      deferred.resolve(this);
+        deferred.resolve(this);
+      });
     }, console.error);
 
     return deferred.promise;
   },
 
   /**
    * Show an error message from the style editor in the toolbox
    * notification box.
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -125,16 +125,21 @@
   -  panel. -->
 <!ENTITY options.styleeditor.label            "Style Editor">
 
 <!-- LOCALIZATION NOTE (options.stylesheetSourceMaps.label): This is the
    - label for the checkbox that toggles showing original sources in the Style Editor -->
 <!ENTITY options.stylesheetSourceMaps.label      "Show original sources">
 <!ENTITY options.stylesheetSourceMaps.tooltip    "Show original sources (e.g. Sass files) in the Style Editor and Inspector">
 
+<!-- LOCALIZATION NOTE (options.stylesheetAutocompletion.label): This is the
+   - label for the checkbox that toggles autocompletion of css in the Style Editor -->
+<!ENTITY options.stylesheetAutocompletion.label      "Autocomplete CSS">
+<!ENTITY options.stylesheetAutocompletion.tooltip    "Autocomplete CSS properties, values and selectors in Style Editor as you type">
+
 <!-- LOCALIZATION NOTE (options.profiler.label): This is the label for the
   -  heading of the group of JavaScript Profiler preferences in the options
   -  panel. -->
 <!ENTITY options.profiler.label            "JavaScript Profiler">
 
 <!-- LOCALIZATION NOTE (options.showPlatformData.label): This is the
   -  label for the checkbox that toggles the display of the platform data in the,
   -  Profiler i.e. devtools.profiler.ui.show-platform-data a boolean preference
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -60,16 +60,48 @@ const object = require("sdk/util/object"
 const events = require("sdk/event/core");
 const {Unknown} = require("sdk/platform/xpcom");
 const {Class} = require("sdk/core/heritage");
 const {PageStyleActor} = require("devtools/server/actors/styles");
 const {HighlighterActor} = require("devtools/server/actors/highlighter");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+// The possible completions to a ':' with added score to give certain values
+// some preference.
+const PSEUDO_SELECTORS = [
+  [":active", 1],
+  [":hover", 1],
+  [":focus", 1],
+  [":visited", 0],
+  [":link", 0],
+  [":first-letter", 0],
+  [":first-child", 2],
+  [":before", 2],
+  [":after", 2],
+  [":lang(", 0],
+  [":not(", 3],
+  [":first-of-type", 0],
+  [":last-of-type", 0],
+  [":only-of-type", 0],
+  [":only-child", 2],
+  [":nth-child(", 3],
+  [":nth-last-child(", 0],
+  [":nth-of-type(", 0],
+  [":nth-last-of-type(", 0],
+  [":last-child", 2],
+  [":root", 0],
+  [":empty", 0],
+  [":target", 0],
+  [":enabled", 0],
+  [":disabled", 0],
+  [":checked", 1],
+  ["::selection", 0]
+];
+
 
 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
@@ -1398,16 +1430,146 @@ var WalkerActor = protocol.ActorClass({
       selector: Arg(1)
     },
     response: {
       list: RetVal("domnodelist")
     }
   }),
 
   /**
+   * Returns a list of matching results for CSS selector autocompletion.
+   *
+   * @param string query
+   *        The selector query being completed
+   * @param string completing
+   *        The exact token being completed out of the query
+   * @param string selectorState
+   *        One of "pseudo", "id", "tag", "class", "null"
+   */
+  getSuggestionsForQuery: method(function(query, completing, selectorState) {
+    let sugs = {
+      classes: new Map,
+      tags: new Map
+    };
+    let result = [];
+    let nodes = null;
+    // Filtering and sorting the results so that protocol transfer is miminal.
+    switch (selectorState) {
+      case "pseudo":
+        result = PSEUDO_SELECTORS.filter(item => {
+          return item[0].startsWith(":" + completing);
+        });
+        break;
+
+      case "class":
+        if (!query) {
+          nodes = this.rootDoc.querySelectorAll("[class]");
+        }
+        else {
+          nodes = this.rootDoc.querySelectorAll(query);
+        }
+        for (let node of nodes) {
+          for (let className of node.className.split(" ")) {
+            sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
+          }
+        }
+        sugs.classes.delete("");
+        // Editing the style editor may make the stylesheet have errors and
+        // thus the page's elements' styles start changing with a transition.
+        // That transition comes from the `moz-styleeditor-transitioning` class.
+        sugs.classes.delete("moz-styleeditor-transitioning");
+        sugs.classes.delete(HIDDEN_CLASS);
+        for (let [className, count] of sugs.classes) {
+          if (className.startsWith(completing)) {
+            result.push(["." + className, count]);
+          }
+        }
+        break;
+
+      case "id":
+        if (!query) {
+          nodes = this.rootDoc.querySelectorAll("[id]");
+        }
+        else {
+          nodes = this.rootDoc.querySelectorAll(query);
+        }
+        for (let node of nodes) {
+          if (node.id.startsWith(completing)) {
+            result.push(["#" + node.id, 1]);
+          }
+        }
+        break;
+
+      case "tag":
+        if (!query) {
+          nodes = this.rootDoc.getElementsByTagName("*");
+        }
+        else {
+          nodes = this.rootDoc.querySelectorAll(query);
+        }
+        for (let node of nodes) {
+          let tag = node.tagName.toLowerCase();
+          sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
+        }
+        for (let [tag, count] of sugs.tags) {
+          if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
+            result.push([tag, count]);
+          }
+        }
+        break;
+
+      case "null":
+        nodes = this.rootDoc.querySelectorAll(query);
+        for (let node of nodes) {
+          node.id && result.push(["#" + node.id, 1]);
+          let tag = node.tagName.toLowerCase();
+          sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
+          for (let className of node.className.split(" ")) {
+            sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
+          }
+        }
+        for (let [tag, count] of sugs.tags) {
+          tag && result.push([tag, count]);
+        }
+        sugs.classes.delete("");
+        // Editing the style editor may make the stylesheet have errors and
+        // thus the page's elements' styles start changing with a transition.
+        // That transition comes from the `moz-styleeditor-transitioning` class.
+        sugs.classes.delete("moz-styleeditor-transitioning");
+        sugs.classes.delete(HIDDEN_CLASS);
+        for (let [className, count] of sugs.classes) {
+          className && result.push(["." + className, count]);
+        }
+    }
+
+    // Sort alphabetically in increaseing order.
+    result = result.sort();
+    // Sort based on count in decreasing order.
+    result = result.sort(function(a, b) {
+      return b[1] - a[1];
+    });
+
+    result.slice(0, 25);
+
+    return {
+      query: query,
+      suggestions: result
+    };
+  }, {
+    request: {
+      query: Arg(0),
+      completing: Arg(1),
+      selectorState: Arg(2)
+    },
+    response: {
+      list: RetVal("array:array:string")
+    }
+  }),
+
+  /**
    * Add a pseudo-class lock to a node.
    *
    * @param NodeActor node
    * @param string pseudo
    *    A pseudoclass: ':hover', ':active', ':focus'
    * @param options
    *    Options object:
    *    `parents`: True if the pseudo-class should be added