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 165534 fcbf8c877de97d3a9206e6e4a6a309f0af2c8f18
parent 165533 30d6d1f88b0028bd05868ad435677408a1b5d3a6
child 165535 025d4a4b4223575aaaf7442f416a064f000d6d79
push id4615
push userscrapmachines@gmail.com
push dateTue, 28 Jan 2014 15:27:15 +0000
treeherderfx-team@cbc8854278fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanton, msucan, dcamp, robcee, harth
bugs717369
milestone29.0a1
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