Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 21 May 2014 14:26:21 +0200
changeset 184182 46fecf8f015cade3d390c0b5ecb3bd28d6dc2ed2
parent 184097 83849e385943482b3974c9f379fa64031d7ce8b1 (current diff)
parent 184181 b9e1856deef13f3b6aac70b5324f40b46284481d (diff)
child 184183 76c5ba501d687a24fba8a21cf973d2c4fb1b5a38
push id43763
push usercbook@mozilla.com
push dateWed, 21 May 2014 12:26:42 +0000
treeherdermozilla-inbound@46fecf8f015c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
addon-sdk/source/python-lib/cuddlefish/options_defaults.py
addon-sdk/source/python-lib/cuddlefish/options_xul.py
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/package.json
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/no-prefs/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/no-prefs/package.json
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/package.json
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/simple-prefs/lib/main.js
addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/simple-prefs/package.json
b2g/components/ContentHandler.js
browser/themes/linux/tabbrowser/tab.png
browser/themes/osx/tabbrowser/tabbar-bottom-bg-active.png
browser/themes/osx/tabbrowser/tabbar-bottom-bg-inactive.png
browser/themes/osx/tabbrowser/tabbar-top-bg-active.png
browser/themes/osx/tabbrowser/tabbar-top-bg-inactive.png
browser/themes/shared/devtools/images/itemToggle-light.png
browser/themes/windows/tabbrowser/tab.png
dom/moz.build
dom/webidl/moz.build
toolkit/mozapps/extensions/test/addons/test_bug542391_1/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_2/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_3_1/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_3_2/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_4/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_5/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_6/install.rdf
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 994964 apparently requires a clobber, unclear why
+Bug 1004726 requires a clobber for B2G Emulator builds
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -1,7 +1,13 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+JS_MODULES_PATH = 'modules/sdk'
+
+EXTRA_JS_MODULES += [
+    'source/app-extension/bootstrap.js',
+]
--- a/addon-sdk/source/app-extension/install.rdf
+++ b/addon-sdk/source/app-extension/install.rdf
@@ -22,11 +22,12 @@
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>Test App</em:name>
     <em:description>Harness for tests.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL></em:homepageURL>
     <em:optionsType></em:optionsType>
+    <em:optionsURL></em:optionsURL>
     <em:updateURL></em:updateURL>
   </Description>
 </RDF>
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -7,17 +7,17 @@ module.metadata = {
 };
 
 const { Cc, Ci } = require('chrome');
 const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
 const { once } = require('../system/events');
 const { exit, env, staticArgs } = require('../system');
 const { when: unload } = require('../system/unload');
 const { loadReason } = require('../self');
-const { rootURI } = require("@loader/options");
+const { rootURI, metadata: { preferences } } = require("@loader/options");
 const globals = require('../system/globals');
 const xulApp = require('../system/xul-app');
 const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
                         getService(Ci.nsIAppShellService);
 
 const NAME2TOPIC = {
   'Firefox': 'sessionstore-windows-restored',
   'Fennec': 'sessionstore-windows-restored',
@@ -124,30 +124,50 @@ function run(options) {
       // disable. Because unit tests are evaluated in a another Loader who
       // doesn't have access to this current loader.
       if (options.main !== 'test-harness/run-tests')
         require('../l10n/html').enable();
     }
     catch(error) {
       console.exception(error);
     }
-    // Initialize inline options localization, without preventing addon to be
-    // run in case of error
-    try {
-      require('../l10n/prefs');
-    }
-    catch(error) {
-      console.exception(error);
-    }
+
+    // native-options does stuff directly with preferences key from package.json
+    if (preferences && preferences.length > 0) {
+      try {
+        require('../preferences/native-options').enable(preferences);
+      } 
+      catch (error) {
+        console.exception(error); 
+      }
+    } 
+    else {
+      // keeping support for addons packaged with older SDK versions, 
+      // when cfx didn't include the 'preferences' key in @loader/options
 
-    // TODO: When bug 564675 is implemented this will no longer be needed
-    // Always set the default prefs, because they disappear on restart
-    if (options.prefsURI) {
-      // Only set if `prefsURI` specified
-      setDefaultPrefs(options.prefsURI);
+      // Initialize inline options localization, without preventing addon to be
+      // run in case of error
+      try {
+        require('../l10n/prefs').enable();
+      }
+      catch(error) {
+        console.exception(error);
+      }
+
+      // TODO: When bug 564675 is implemented this will no longer be needed
+      // Always set the default prefs, because they disappear on restart
+      if (options.prefsURI) {
+        // Only set if `prefsURI` specified
+        try {
+          setDefaultPrefs(options.prefsURI);
+        } 
+        catch (err) {
+          // cfx bootstrap always passes prefsURI, even in addons without prefs
+        }
+      }
     }
 
     // this is where the addon's main.js finally run.
     let program = main(options.loader, options.main);
 
     if (typeof(program.onUnload) === 'function')
       unload(program.onUnload);
 
--- a/addon-sdk/source/lib/sdk/event/target.js
+++ b/addon-sdk/source/lib/sdk/event/target.js
@@ -4,17 +4,17 @@
 
 'use strict';
 
 module.metadata = {
   "stability": "stable"
 };
 
 const { on, once, off, setListeners } = require('./core');
-const { method, chainable } = require('../lang/functional');
+const { method, chainable } = require('../lang/functional/core');
 const { Class } = require('../core/heritage');
 
 /**
  * `EventTarget` is an exemplar for creating an objects that can be used to
  * add / remove event listeners on them. Events on these objects may be emitted
  * via `emit` function exported by 'event/core' module.
  */
 const EventTarget = Class({
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js
+++ b/addon-sdk/source/lib/sdk/l10n/prefs.js
@@ -1,22 +1,31 @@
 /* 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/. */
 "use strict";
 
 const { on } = require("../system/events");
 const core = require("./core");
-const { id: jetpackId} = require('../self');
+const { id: jetpackId } = require('../self');
 
 const OPTIONS_DISPLAYED = "addon-options-displayed";
 
+function enable() {
+  on(OPTIONS_DISPLAYED, onOptionsDisplayed);  
+}
+exports.enable = enable;
+
 function onOptionsDisplayed({ subject: document, data: addonId }) {
   if (addonId !== jetpackId)
     return;
+  localizeInlineOptions(document);
+}
+
+function localizeInlineOptions(document) {
   let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
               'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
   let nodes = document.querySelectorAll(query);
   for (let node of nodes) {
     let name = node.getAttribute("pref-name");
     if (node.tagName == "setting") {
       let desc = core.get(name + "_description");
       if (desc)
@@ -34,9 +43,9 @@ function onOptionsDisplayed({ subject: d
     }
     else if (node.tagName == "button") {
       let label = core.get(name + "_label");
       if (label)
         node.setAttribute("label", label);
     }
   }
 }
-on(OPTIONS_DISPLAYED, onOptionsDisplayed);
+exports.localizeInlineOptions = localizeInlineOptions;
--- a/addon-sdk/source/lib/sdk/lang/functional.js
+++ b/addon-sdk/source/lib/sdk/lang/functional.js
@@ -7,394 +7,41 @@
 // those goes to him.
 
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-const { deprecateFunction } = require("../util/deprecate");
-const { setImmediate, setTimeout, clearTimeout } = require("../timers");
-
-const arity = f => f.arity || f.length;
-
-const name = f => f.displayName || f.name;
-
-const derive = (f, source) => {
-  f.displayName = name(source);
-  f.arity = arity(source);
-  return f;
-};
-
-/**
- * Takes variadic numeber of functions and returns composed one.
- * Returned function pushes `this` pseudo-variable to the head
- * of the passed arguments and invokes all the functions from
- * left to right passing same arguments to them. Composite function
- * returns return value of the right most funciton.
- */
-const method = (...lambdas) => {
-  return function method(...args) {
-    args.unshift(this);
-    return lambdas.reduce((_, lambda) => lambda.apply(this, args),
-                          void(0));
-  };
-};
-exports.method = method;
-
-/**
- * Takes a function and returns a wrapped one instead, calling which will call
- * original function in the next turn of event loop. This is basically utility
- * to do `setImmediate(function() { ... })`, with a difference that returned
- * function is reused, instead of creating a new one each time. This also allows
- * to use this functions as event listeners.
- */
-const defer = f => derive(function(...args) {
-  setImmediate(invoke, f, args, this);
-}, f);
-exports.defer = defer;
-// Exporting `remit` alias as `defer` may conflict with promises.
-exports.remit = defer;
-
-/**
- * Invokes `callee` by passing `params` as an arguments and `self` as `this`
- * pseudo-variable. Returns value that is returned by a callee.
- * @param {Function} callee
- *    Function to invoke.
- * @param {Array} params
- *    Arguments to invoke function with.
- * @param {Object} self
- *    Object to be passed as a `this` pseudo variable.
- */
-const invoke = (callee, params, self) => callee.apply(self, params);
-exports.invoke = invoke;
-
-/**
- * Takes a function and bind values to one or more arguments, returning a new
- * function of smaller arity.
- *
- * @param {Function} fn
- *    The function to partial
- *
- * @returns The new function with binded values
- */
-const partial = (f, ...curried) => {
-  if (typeof(f) !== "function")
-    throw new TypeError(String(f) + " is not a function");
-
-  let fn = derive(function(...args) {
-    return f.apply(this, curried.concat(args));
-  }, f);
-  fn.arity = arity(f) - curried.length;
-  return fn;
-};
-exports.partial = partial;
+const { defer, remit, delay, debounce,
+        throttle } = require("./functional/concurrent");
+const { method, invoke, partial, curry, compose, wrap, identity, memoize, once,
+        cache, complement, constant, when, apply, flip, field, query,
+        isInstance, chainable, is, isnt } = require("./functional/core");
 
-/**
- * Returns function with implicit currying, which will continue currying until
- * expected number of argument is collected. Expected number of arguments is
- * determined by `fn.length`. Using this with variadic functions is stupid,
- * so don't do it.
- *
- * @examples
- *
- * var sum = curry(function(a, b) {
- *   return a + b
- * })
- * console.log(sum(2, 2)) // 4
- * console.log(sum(2)(4)) // 6
- */
-const curry = new function() {
-  const currier = (fn, arity, params) => {
-    // Function either continues to curry arguments or executes function
-    // if desired arguments have being collected.
-    const curried = function(...input) {
-      // Prepend all curried arguments to the given arguments.
-      if (params) input.unshift.apply(input, params);
-      // If expected number of arguments has being collected invoke fn,
-      // othrewise return curried version Otherwise continue curried.
-      return (input.length >= arity) ? fn.apply(this, input) :
-             currier(fn, arity, input);
-    };
-    curried.arity = arity - (params ? params.length : 0);
-
-    return curried;
-  };
-
-  return fn => currier(fn, arity(fn));
-};
-exports.curry = curry;
-
-/**
- * Returns the composition of a list of functions, where each function consumes
- * the return value of the function that follows. In math terms, composing the
- * functions `f()`, `g()`, and `h()` produces `f(g(h()))`.
- * @example
- *
- *   var greet = function(name) { return "hi: " + name; };
- *   var exclaim = function(statement) { return statement + "!"; };
- *   var welcome = compose(exclaim, greet);
- *
- *   welcome('moe');    // => 'hi: moe!'
- */
-function compose(...lambdas) {
-  return function composed(...args) {
-    let index = lambdas.length;
-    while (0 <= --index)
-      args = [lambdas[index].apply(this, args)];
-
-    return args[0];
-  };
-}
-exports.compose = compose;
-
-/*
- * Returns the first function passed as an argument to the second,
- * allowing you to adjust arguments, run code before and after, and
- * conditionally execute the original function.
- * @example
- *
- *  var hello = function(name) { return "hello: " + name; };
- *  hello = wrap(hello, function(f) {
- *    return "before, " + f("moe") + ", after";
- *  });
- *
- *  hello();    // => 'before, hello: moe, after'
- */
-const wrap = (f, wrapper) => derive(function wrapped(...args) {
-  return wrapper.apply(this, [f].concat(args));
-}, f);
-exports.wrap = wrap;
-
-/**
- * Returns the same value that is used as the argument. In math: f(x) = x
- */
-const identity = value => value;
-exports.identity = identity;
-
-/**
- * Memoizes a given function by caching the computed result. Useful for
- * speeding up slow-running computations. If passed an optional hashFunction,
- * it will be used to compute the hash key for storing the result, based on
- * the arguments to the original function. The default hashFunction just uses
- * the first argument to the memoized function as the key.
- */
-const memoize = (f, hasher) => {
-  let memo = Object.create(null);
-  let cache = new WeakMap();
-  hasher = hasher || identity;
-  return derive(function memoizer(...args) {
-    const key = hasher.apply(this, args);
-    const type = typeof(key);
-    if (key && (type === "object" || type === "function")) {
-      if (!cache.has(key))
-        cache.set(key, f.apply(this, args));
-      return cache.get(key);
-    }
-    else {
-      if (!(key in memo))
-        memo[key] = f.apply(this, args);
-      return memo[key];
-    }
-  }, f);
-};
-exports.memoize = memoize;
+exports.defer = defer;
+exports.remit = remit;
+exports.delay = delay;
+exports.debounce = debounce;
+exports.throttle = throttle;
 
-/**
- * Much like setTimeout, invokes function after wait milliseconds. If you pass
- * the optional arguments, they will be forwarded on to the function when it is
- * invoked.
- */
-const delay = function delay(f, ms, ...args) {
-  setTimeout(() => f.apply(this, args), ms);
-};
-exports.delay = delay;
-
-/**
- * Creates a version of the function that can only be called one time. Repeated
- * calls to the modified function will have no effect, returning the value from
- * the original call. Useful for initialization functions, instead of having to
- * set a boolean flag and then check it later.
- */
-const once = f => {
-  let ran = false, cache;
-  return derive(function(...args) {
-    return ran ? cache : (ran = true, cache = f.apply(this, args));
-  }, f);
-};
+exports.method = method;
+exports.invoke = invoke;
+exports.partial = partial;
+exports.curry = curry;
+exports.compose = compose;
+exports.wrap = wrap;
+exports.identity = identity;
+exports.memoize = memoize;
 exports.once = once;
-// export cache as once will may be conflicting with event once a lot.
-exports.cache = once;
-
-// Takes a `f` function and returns a function that takes the same
-// arguments as `f`, has the same effects, if any, and returns the
-// opposite truth value.
-const complement = f => derive(function(...args) {
-  return args.length < arity(f) ? complement(partial(f, ...args)) :
-         !f.apply(this, args);
-}, f);
+exports.cache = cache;
 exports.complement = complement;
-
-// Constructs function that returns `x` no matter what is it
-// invoked with.
-const constant = x => _ => x;
 exports.constant = constant;
-
-// Takes `p` predicate, `consequent` function and an optional
-// `alternate` function and composes function that returns
-// application of arguments over `consequent` if application over
-// `p` is `true` otherwise returns application over `alternate`.
-// If `alternate` is not a function returns `undefined`.
-const when = (p, consequent, alternate) => {
-  if (typeof(alternate) !== "function" && alternate !== void(0))
-    throw TypeError("alternate must be a function");
-  if (typeof(consequent) !== "function")
-    throw TypeError("consequent must be a function");
-
-  return function(...args) {
-    return p.apply(this, args) ?
-           consequent.apply(this, args) :
-           alternate && alternate.apply(this, args);
-  };
-};
 exports.when = when;
-
-// Apply function that behaves as `apply` does in lisp:
-// apply(f, x, [y, z]) => f.apply(f, [x, y, z])
-// apply(f, x) => f.apply(f, [x])
-const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop()));
 exports.apply = apply;
-
-// Returns function identical to given `f` but with flipped order
-// of arguments.
-const flip = f => derive(function(...args) {
-  return f.apply(this, args.reverse());
-}, f);
 exports.flip = flip;
-
-
-// Takes field `name` and `target` and returns value of that field.
-// If `target` is `null` or `undefined` it would be returned back
-// instead of attempt to access it's field. Function is implicitly
-// curried, this allows accessor function generation by calling it
-// with only `name` argument.
-const field = curry((name, target) =>
-  // Note: Permisive `==` is intentional.
-  target == null ? target : target[name]);
 exports.field = field;
-
-// Takes `.` delimited string representing `path` to a nested field
-// and a `target` to get it from. For convinience function is
-// implicitly curried, there for accessors can be created by invoking
-// it with just a `path` argument.
-const query = curry((path, target) => {
-  const names = path.split(".");
-  const count = names.length;
-  let index = 0;
-  let result = target;
-  // Note: Permisive `!=` is intentional.
-  while (result != null && index < count) {
-    result = result[names[index]];
-    index = index + 1;
-  }
-  return result;
-});
 exports.query = query;
-
-// Takes `Type` (constructor function) and a `value` and returns
-// `true` if `value` is instance of the given `Type`. Function is
-// implicitly curried this allows predicate generation by calling
-// function with just first argument.
-const isInstance = curry((Type, value) => value instanceof Type);
 exports.isInstance = isInstance;
-
-/*
- * Takes a funtion and returns a wrapped function that returns `this`
- */
-const chainable = f => derive(function(...args) {
-  f.apply(this, args);
-  return this;
-}, f);
 exports.chainable = chainable;
-exports.chain =
-  deprecateFunction(chainable, "Function `chain` was renamed to `chainable`");
-
-// Functions takes `expected` and `actual` values and returns `true` if
-// `expected === actual`. Returns curried function if called with less then
-// two arguments.
-//
-// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
-const is = curry((expected, actual) => actual === expected);
 exports.is = is;
-
-const isnt = complement(is);
 exports.isnt = isnt;
-
-/**
- * From underscore's `_.debounce`
- * http://underscorejs.org
- * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
- * Underscore may be freely distributed under the MIT license.
- */
-const debounce = function debounce (fn, wait) {
-  let timeout, args, context, timestamp, result;
-
-  let later = function () {
-    let last = Date.now() - timestamp;
-    if (last < wait) {
-      timeout = setTimeout(later, wait - last);
-    } else {
-      timeout = null;
-      result = fn.apply(context, args);
-      context = args = null;
-    }
-  };
-
-  return function (...aArgs) {
-    context = this;
-    args = aArgs;
-    timestamp  = Date.now();
-    if (!timeout) {
-      timeout = setTimeout(later, wait);
-    }
-
-    return result;
-  };
-};
-exports.debounce = debounce;
-
-/**
- * From underscore's `_.throttle`
- * http://underscorejs.org
- * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
- * Underscore may be freely distributed under the MIT license.
- */
-const throttle = function throttle (func, wait, options) {
-  let context, args, result;
-  let timeout = null;
-  let previous = 0;
-  options || (options = {});
-  let later = function() {
-    previous = options.leading === false ? 0 : Date.now();
-    timeout = null;
-    result = func.apply(context, args);
-    context = args = null;
-  };
-  return function() {
-    let now = Date.now();
-    if (!previous && options.leading === false) previous = now;
-    let remaining = wait - (now - previous);
-    context = this;
-    args = arguments;
-    if (remaining <= 0) {
-      clearTimeout(timeout);
-      timeout = null;
-      previous = now;
-      result = func.apply(context, args);
-      context = args = null;
-    } else if (!timeout && options.trailing !== false) {
-      timeout = setTimeout(later, remaining);
-    }
-    return result;
-  };
-};
-exports.throttle = throttle;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/concurrent.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+// Disclaimer: Some of the functions in this module implement APIs from
+// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
+// those goes to him.
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { arity, name, derive, invoke } = require("./helpers");
+const { setTimeout, clearTimeout, setImmediate } = require("../../timers");
+
+/**
+ * Takes a function and returns a wrapped one instead, calling which will call
+ * original function in the next turn of event loop. This is basically utility
+ * to do `setImmediate(function() { ... })`, with a difference that returned
+ * function is reused, instead of creating a new one each time. This also allows
+ * to use this functions as event listeners.
+ */
+const defer = f => derive(function(...args) {
+  setImmediate(invoke, f, args, this);
+}, f);
+exports.defer = defer;
+// Exporting `remit` alias as `defer` may conflict with promises.
+exports.remit = defer;
+
+/**
+ * Much like setTimeout, invokes function after wait milliseconds. If you pass
+ * the optional arguments, they will be forwarded on to the function when it is
+ * invoked.
+ */
+const delay = function delay(f, ms, ...args) {
+  setTimeout(() => f.apply(this, args), ms);
+};
+exports.delay = delay;
+
+/**
+ * From underscore's `_.debounce`
+ * http://underscorejs.org
+ * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Underscore may be freely distributed under the MIT license.
+ */
+const debounce = function debounce (fn, wait) {
+  let timeout, args, context, timestamp, result;
+
+  let later = function () {
+    let last = Date.now() - timestamp;
+    if (last < wait) {
+      timeout = setTimeout(later, wait - last);
+    } else {
+      timeout = null;
+      result = fn.apply(context, args);
+      context = args = null;
+    }
+  };
+
+  return function (...aArgs) {
+    context = this;
+    args = aArgs;
+    timestamp  = Date.now();
+    if (!timeout) {
+      timeout = setTimeout(later, wait);
+    }
+
+    return result;
+  };
+};
+exports.debounce = debounce;
+
+/**
+ * From underscore's `_.throttle`
+ * http://underscorejs.org
+ * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Underscore may be freely distributed under the MIT license.
+ */
+const throttle = function throttle (func, wait, options) {
+  let context, args, result;
+  let timeout = null;
+  let previous = 0;
+  options || (options = {});
+  let later = function() {
+    previous = options.leading === false ? 0 : Date.now();
+    timeout = null;
+    result = func.apply(context, args);
+    context = args = null;
+  };
+  return function() {
+    let now = Date.now();
+    if (!previous && options.leading === false) previous = now;
+    let remaining = wait - (now - previous);
+    context = this;
+    args = arguments;
+    if (remaining <= 0) {
+      clearTimeout(timeout);
+      timeout = null;
+      previous = now;
+      result = func.apply(context, args);
+      context = args = null;
+    } else if (!timeout && options.trailing !== false) {
+      timeout = setTimeout(later, remaining);
+    }
+    return result;
+  };
+};
+exports.throttle = throttle;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/core.js
@@ -0,0 +1,290 @@
+/* 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/. */
+
+// Disclaimer: Some of the functions in this module implement APIs from
+// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
+// those goes to him.
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+}
+const { arity, name, derive, invoke } = require("./helpers");
+
+/**
+ * Takes variadic numeber of functions and returns composed one.
+ * Returned function pushes `this` pseudo-variable to the head
+ * of the passed arguments and invokes all the functions from
+ * left to right passing same arguments to them. Composite function
+ * returns return value of the right most funciton.
+ */
+const method = (...lambdas) => {
+  return function method(...args) {
+    args.unshift(this);
+    return lambdas.reduce((_, lambda) => lambda.apply(this, args),
+                          void(0));
+  };
+};
+exports.method = method;
+
+/**
+ * Invokes `callee` by passing `params` as an arguments and `self` as `this`
+ * pseudo-variable. Returns value that is returned by a callee.
+ * @param {Function} callee
+ *    Function to invoke.
+ * @param {Array} params
+ *    Arguments to invoke function with.
+ * @param {Object} self
+ *    Object to be passed as a `this` pseudo variable.
+ */
+exports.invoke = invoke;
+
+/**
+ * Takes a function and bind values to one or more arguments, returning a new
+ * function of smaller arity.
+ *
+ * @param {Function} fn
+ *    The function to partial
+ *
+ * @returns The new function with binded values
+ */
+const partial = (f, ...curried) => {
+  if (typeof(f) !== "function")
+    throw new TypeError(String(f) + " is not a function");
+
+  let fn = derive(function(...args) {
+    return f.apply(this, curried.concat(args));
+  }, f);
+  fn.arity = arity(f) - curried.length;
+  return fn;
+};
+exports.partial = partial;
+
+/**
+ * Returns function with implicit currying, which will continue currying until
+ * expected number of argument is collected. Expected number of arguments is
+ * determined by `fn.length`. Using this with variadic functions is stupid,
+ * so don't do it.
+ *
+ * @examples
+ *
+ * var sum = curry(function(a, b) {
+ *   return a + b
+ * })
+ * console.log(sum(2, 2)) // 4
+ * console.log(sum(2)(4)) // 6
+ */
+const curry = new function() {
+  const currier = (fn, arity, params) => {
+    // Function either continues to curry arguments or executes function
+    // if desired arguments have being collected.
+    const curried = function(...input) {
+      // Prepend all curried arguments to the given arguments.
+      if (params) input.unshift.apply(input, params);
+      // If expected number of arguments has being collected invoke fn,
+      // othrewise return curried version Otherwise continue curried.
+      return (input.length >= arity) ? fn.apply(this, input) :
+             currier(fn, arity, input);
+    };
+    curried.arity = arity - (params ? params.length : 0);
+
+    return curried;
+  };
+
+  return fn => currier(fn, arity(fn));
+};
+exports.curry = curry;
+
+/**
+ * Returns the composition of a list of functions, where each function consumes
+ * the return value of the function that follows. In math terms, composing the
+ * functions `f()`, `g()`, and `h()` produces `f(g(h()))`.
+ * @example
+ *
+ *   var greet = function(name) { return "hi: " + name; };
+ *   var exclaim = function(statement) { return statement + "!"; };
+ *   var welcome = compose(exclaim, greet);
+ *
+ *   welcome('moe');    // => 'hi: moe!'
+ */
+function compose(...lambdas) {
+  return function composed(...args) {
+    let index = lambdas.length;
+    while (0 <= --index)
+      args = [lambdas[index].apply(this, args)];
+
+    return args[0];
+  };
+}
+exports.compose = compose;
+
+/*
+ * Returns the first function passed as an argument to the second,
+ * allowing you to adjust arguments, run code before and after, and
+ * conditionally execute the original function.
+ * @example
+ *
+ *  var hello = function(name) { return "hello: " + name; };
+ *  hello = wrap(hello, function(f) {
+ *    return "before, " + f("moe") + ", after";
+ *  });
+ *
+ *  hello();    // => 'before, hello: moe, after'
+ */
+const wrap = (f, wrapper) => derive(function wrapped(...args) {
+  return wrapper.apply(this, [f].concat(args));
+}, f);
+exports.wrap = wrap;
+
+/**
+ * Returns the same value that is used as the argument. In math: f(x) = x
+ */
+const identity = value => value;
+exports.identity = identity;
+
+/**
+ * Memoizes a given function by caching the computed result. Useful for
+ * speeding up slow-running computations. If passed an optional hashFunction,
+ * it will be used to compute the hash key for storing the result, based on
+ * the arguments to the original function. The default hashFunction just uses
+ * the first argument to the memoized function as the key.
+ */
+const memoize = (f, hasher) => {
+  let memo = Object.create(null);
+  let cache = new WeakMap();
+  hasher = hasher || identity;
+  return derive(function memoizer(...args) {
+    const key = hasher.apply(this, args);
+    const type = typeof(key);
+    if (key && (type === "object" || type === "function")) {
+      if (!cache.has(key))
+        cache.set(key, f.apply(this, args));
+      return cache.get(key);
+    }
+    else {
+      if (!(key in memo))
+        memo[key] = f.apply(this, args);
+      return memo[key];
+    }
+  }, f);
+};
+exports.memoize = memoize;
+
+/*
+ * Creates a version of the function that can only be called one time. Repeated
+ * calls to the modified function will have no effect, returning the value from
+ * the original call. Useful for initialization functions, instead of having to
+ * set a boolean flag and then check it later.
+ */
+const once = f => {
+  let ran = false, cache;
+  return derive(function(...args) {
+    return ran ? cache : (ran = true, cache = f.apply(this, args));
+  }, f);
+};
+exports.once = once;
+// export cache as once will may be conflicting with event once a lot.
+exports.cache = once;
+
+// Takes a `f` function and returns a function that takes the same
+// arguments as `f`, has the same effects, if any, and returns the
+// opposite truth value.
+const complement = f => derive(function(...args) {
+  return args.length < arity(f) ? complement(partial(f, ...args)) :
+         !f.apply(this, args);
+}, f);
+exports.complement = complement;
+
+// Constructs function that returns `x` no matter what is it
+// invoked with.
+const constant = x => _ => x;
+exports.constant = constant;
+
+// Takes `p` predicate, `consequent` function and an optional
+// `alternate` function and composes function that returns
+// application of arguments over `consequent` if application over
+// `p` is `true` otherwise returns application over `alternate`.
+// If `alternate` is not a function returns `undefined`.
+const when = (p, consequent, alternate) => {
+  if (typeof(alternate) !== "function" && alternate !== void(0))
+    throw TypeError("alternate must be a function");
+  if (typeof(consequent) !== "function")
+    throw TypeError("consequent must be a function");
+
+  return function(...args) {
+    return p.apply(this, args) ?
+           consequent.apply(this, args) :
+           alternate && alternate.apply(this, args);
+  };
+};
+exports.when = when;
+
+// Apply function that behaves as `apply` does in lisp:
+// apply(f, x, [y, z]) => f.apply(f, [x, y, z])
+// apply(f, x) => f.apply(f, [x])
+const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop()));
+exports.apply = apply;
+
+// Returns function identical to given `f` but with flipped order
+// of arguments.
+const flip = f => derive(function(...args) {
+  return f.apply(this, args.reverse());
+}, f);
+exports.flip = flip;
+
+// Takes field `name` and `target` and returns value of that field.
+// If `target` is `null` or `undefined` it would be returned back
+// instead of attempt to access it's field. Function is implicitly
+// curried, this allows accessor function generation by calling it
+// with only `name` argument.
+const field = curry((name, target) =>
+  // Note: Permisive `==` is intentional.
+  target == null ? target : target[name]);
+exports.field = field;
+
+// Takes `.` delimited string representing `path` to a nested field
+// and a `target` to get it from. For convinience function is
+// implicitly curried, there for accessors can be created by invoking
+// it with just a `path` argument.
+const query = curry((path, target) => {
+  const names = path.split(".");
+  const count = names.length;
+  let index = 0;
+  let result = target;
+  // Note: Permisive `!=` is intentional.
+  while (result != null && index < count) {
+    result = result[names[index]];
+    index = index + 1;
+  }
+  return result;
+});
+exports.query = query;
+
+// Takes `Type` (constructor function) and a `value` and returns
+// `true` if `value` is instance of the given `Type`. Function is
+// implicitly curried this allows predicate generation by calling
+// function with just first argument.
+const isInstance = curry((Type, value) => value instanceof Type);
+exports.isInstance = isInstance;
+
+/*
+ * Takes a funtion and returns a wrapped function that returns `this`
+ */
+const chainable = f => derive(function(...args) {
+  f.apply(this, args);
+  return this;
+}, f);
+exports.chainable = chainable;
+
+// Functions takes `expected` and `actual` values and returns `true` if
+// `expected === actual`. Returns curried function if called with less then
+// two arguments.
+//
+// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
+const is = curry((expected, actual) => actual === expected);
+exports.is = is;
+
+const isnt = complement(is);
+exports.isnt = isnt;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/lang/functional/helpers.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+// Disclaimer: Some of the functions in this module implement APIs from
+// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for
+// those goes to him.
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+}
+
+const arity = f => f.arity || f.length;
+exports.arity = arity;
+
+const name = f => f.displayName || f.name;
+exports.name = name;
+
+const derive = (f, source) => {
+  f.displayName = name(source);
+  f.arity = arity(source);
+  return f;
+};
+exports.derive = derive;
+
+const invoke = (callee, params, self) => callee.apply(self, params);
+exports.invoke = invoke;
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -14,17 +14,17 @@ const { getAttachEventType, WorkerHost }
 const { Class } = require('./core/heritage');
 const { Disposable } = require('./core/disposable');
 const { WeakReference } = require('./core/reference');
 const { Worker } = require('./content/worker');
 const { EventTarget } = require('./event/target');
 const { on, emit, once, setListeners } = require('./event/core');
 const { on: domOn, removeListener: domOff } = require('./dom/events');
 const { pipe } = require('./event/utils');
-const { isRegExp } = require('./lang/type');
+const { isRegExp, isUndefined } = require('./lang/type');
 const { merge } = require('./util/object');
 const { windowIterator } = require('./deprecated/window-utils');
 const { isBrowser, getFrames } = require('./window/utils');
 const { getTabs, getTabContentWindow, getTabForContentWindow,
         getURI: getTabURI } = require('./tabs/utils');
 const { ignoreWindow } = require('./private-browsing/utils');
 const { Style } = require("./stylesheet/style");
 const { attach, detach } = require("./content/mod");
@@ -43,17 +43,19 @@ const models = new WeakMap();
 let modelFor = (mod) => models.get(mod);
 let workerFor = (mod) => workers.get(mod);
 let styleFor = (mod) => styles.get(mod);
 
 // Bind observer
 observers.on('document-element-inserted', onContentWindow);
 unload(() => observers.off('document-element-inserted', onContentWindow));
 
+// Helper functions
 let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
+let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
 
 // Validation Contracts
 const modOptions = {
   // contentStyle* / contentScript* are sharing the same validation constraints,
   // so they can be mostly reused, except for the messages.
   contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
     msg: 'The `contentStyle` option must be a string or an array of strings.'
   }),
@@ -66,16 +68,29 @@ const modOptions = {
       if (isRegExpOrString(rule))
         return true;
       if (Array.isArray(rule) && rule.length > 0)
         return rule.every(isRegExpOrString);
       return false;
     },
     msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.'
   },
+  exclude: {
+    is: ['string', 'array', 'regexp', 'undefined'],
+    ok: (rule) => {
+      if (isRegExpOrString(rule) || isUndefined(rule))
+        return true;
+      if (Array.isArray(rule) && rule.length > 0)
+        return rule.every(isRegExpOrString);
+      return false;
+    },
+    msg: 'If set, the `exclude` option must always contain at least one ' +
+      'rule as a string, regular expression, or an array of strings and ' +
+      'regular expressions.'
+  },
   attachTo: {
     is: ['string', 'array', 'undefined'],
     map: function (attachTo) {
       if (!attachTo) return ['top', 'frame'];
       if (typeof attachTo === 'string') return [attachTo];
       return attachTo;
     },
     ok: function (attachTo) {
@@ -110,16 +125,20 @@ const PageMod = Class({
     // Set listeners on {PageMod} itself, not the underlying worker,
     // like `onMessage`, as it'll get piped.
     setListeners(this, options);
 
     let include = model.include;
     model.include = Rules();
     model.include.add.apply(model.include, [].concat(include));
 
+    let exclude = isUndefined(model.exclude) ? [] : model.exclude;
+    model.exclude = Rules();
+    model.exclude.add.apply(model.exclude, [].concat(exclude));
+
     if (model.contentStyle || model.contentStyleFile) {
       styles.set(mod, Style({
         uri: model.contentStyleFile,
         source: model.contentStyle
       }));
     }
 
     pagemods.add(this);
@@ -157,32 +176,32 @@ function onContentWindow({ subject: docu
     return;
 
   // When the tab is private, only addons with 'private-browsing' flag in
   // their package.json can apply content script to private documents
   if (ignoreWindow(window))
     return;
 
   for (let pagemod of pagemods) {
-    if (pagemod.include.matchesAny(document.URL))
+    if (modMatchesURI(pagemod, document.URL))
       onContent(pagemod, window);
   }
 }
 
 function applyOnExistingDocuments (mod) {
   getTabs().forEach(tab => {
     // Fake a newly created document
     let window = getTabContentWindow(tab);
-    if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab)))
+    let uri = getTabURI(tab);
+    if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
       onContent(mod, window);
-    if (has(mod.attachTo, "frame")) {
+    if (has(mod.attachTo, "frame"))
       getFrames(window).
-        filter((iframe) => mod.include.matchesAny(iframe.location.href)).
-        forEach((frame) => onContent(mod, frame));
-    }
+        filter(iframe => modMatchesURI(mod, iframe.location.href)).
+        forEach(frame => onContent(mod, frame));
   });
 }
 
 function createWorker (mod, window) {
   let worker = Worker({
     window: window,
     contentScript: mod.contentScript,
     contentScriptFile: mod.contentScriptFile,
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/preferences/native-options.js
@@ -0,0 +1,157 @@
+/* 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/. */
+'use strict';
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const { on } = require('../system/events');
+const { id, preferencesBranch } = require('../self');
+const { localizeInlineOptions } = require('../l10n/prefs');
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const DEFAULT_OPTIONS_URL = 'data:text/xml,<placeholder/>';
+
+const VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color', 
+                          'file', 'directory', 'control', 'menulist', 'radio'];
+
+function enable(preferences) {
+  validate(preferences);
+  setDefaults(preferences, preferencesBranch);
+
+  // allow the use of custom options.xul
+  AddonManager.getAddonByID(id, (addon) => {
+    if (addon.optionsURL === DEFAULT_OPTIONS_URL) 
+      on('addon-options-displayed', onAddonOptionsDisplayed, true);
+  })
+
+  function onAddonOptionsDisplayed({ subject: doc, data }) {
+    if (data === id) {
+      let parent = doc.getElementById('detail-downloads').parentNode;
+      injectOptions(preferences, preferencesBranch, doc, parent);
+      localizeInlineOptions(doc);
+    }
+  }
+}
+exports.enable = enable;
+
+// centralized sanity checks
+function validate(preferences) {
+  for (let { name, title, type, label, options } of preferences) {
+    // make sure the title is set and non-empty
+    if (!title)
+      throw Error("The '" + name + "' pref requires a title");
+
+    // make sure that pref type is a valid inline option type
+    if (!~VALID_PREF_TYPES.indexOf(type))
+      throw Error("The '" + name + "' pref must be of valid type");
+
+    // if it's a control, make sure it has a label
+    if (type === 'control' && !label)
+      throw Error("The '" + name + "' control requires a label");
+
+    // if it's a menulist or radio, make sure it has options
+    if (type === 'menulist' || type === 'radio') {
+      if (!options)
+        throw Error("The '" + name + "' pref requires options");
+
+      // make sure each option has a value and a label
+      for (let item of options) {
+        if (!('value' in item) || !('label' in item))
+          throw Error("Each option requires both a value and a label");
+      }
+    }
+
+    // TODO: check that pref type matches default value type
+  }
+}
+exports.validate = validate;
+
+// initializes default preferences, emulates defaults/prefs.js
+function setDefaults(preferences, preferencesBranch) {
+  const branch = Cc['@mozilla.org/preferences-service;1'].
+                 getService(Ci.nsIPrefService).
+                 getDefaultBranch('extensions.' + preferencesBranch + '.');
+  for (let {name, value} of preferences) {
+    switch (typeof value) {
+      case 'boolean':
+        branch.setBoolPref(name, value);
+        break;
+      case 'number': 
+        // must be integer, ignore otherwise
+        if (value % 1 === 0) 
+          branch.setIntPref(name, value);
+        break;
+      case 'string':
+        // ∵ 
+        let str = Cc["@mozilla.org/supports-string;1"].
+                  createInstance(Ci.nsISupportsString);
+        str.data = value;
+        branch.setComplexValue(name, Ci.nsISupportsString, str);
+        break;
+    }
+  }
+}
+exports.setDefaults = setDefaults;
+
+// dynamically injects inline options into about:addons page at runtime
+function injectOptions(preferences, preferencesBranch, document, parent) {
+  for (let { name, type, hidden, title, description, label, options, on, off } of preferences) {
+
+    if (hidden) 
+      continue;
+
+    let setting = document.createElement('setting');
+    setting.setAttribute('pref-name', name);
+    setting.setAttribute('data-jetpack-id', id);
+    setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name);
+    setting.setAttribute('type', type);
+    setting.setAttribute('title', title);
+    setting.setAttribute('desc', description);
+
+    if (type === 'file' || type === 'directory') {
+      setting.setAttribute('fullpath', 'true');
+    } 
+    else if (type === 'control') {
+      let button = document.createElement('button');
+      button.setAttribute('pref-name', name);
+      button.setAttribute('data-jetpack-id', id);
+      button.setAttribute('label', label);
+      button.setAttribute('oncommand', "Services.obs.notifyObservers(null, '" +
+                                        id + "-cmdPressed', '" + name + "');");
+      setting.appendChild(button);
+    } 
+    else if (type === 'boolint') {
+      setting.setAttribute('on', on);
+      setting.setAttribute('off', off);
+    } 
+    else if (type === 'menulist') {
+      let menulist = document.createElement('menulist');
+      let menupopup = document.createElement('menupopup');
+      for (let { value, label } of options) {
+        let menuitem = document.createElement('menuitem');
+        menuitem.setAttribute('value', value);
+        menuitem.setAttribute('label', label);
+        menupopup.appendChild(menuitem);
+      }
+      menulist.appendChild(menupopup);
+      setting.appendChild(menulist);
+    } 
+    else if (type === 'radio') {
+      let radiogroup = document.createElement('radiogroup');
+      for (let { value, label } of options) {
+        let radio = document.createElement('radio');
+        radio.setAttribute('value', value);
+        radio.setAttribute('label', label);
+        radiogroup.appendChild(radio);
+      }
+      setting.appendChild(radiogroup);
+    }
+
+    parent.appendChild(setting);
+  }
+}
+exports.injectOptions = injectOptions;
--- a/addon-sdk/source/python-lib/cuddlefish/__init__.py
+++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py
@@ -810,19 +810,16 @@ def run(arguments=sys.argv[1:], target_c
     if options.templatedir:
         app_extension_dir = os.path.abspath(options.templatedir)
     elif os.path.exists(os.path.join(options.pkgdir, "app-extension")):
       app_extension_dir = os.path.join(options.pkgdir, "app-extension")
     else:
         mydir = os.path.dirname(os.path.abspath(__file__))
         app_extension_dir = os.path.join(mydir, "../../app-extension")
 
-    if target_cfg.get('preferences'):
-        harness_options['preferences'] = target_cfg.get('preferences')
-
     # Do not add entries for SDK modules
     harness_options['manifest'] = manifest.get_harness_options_manifest(False)
 
     # Gives an hint to tell if sdk modules are bundled or not
     harness_options['is-sdk-bundled'] = options.bundle_sdk or options.no_strip_xpi
 
     if options.force_use_bundled_sdk:
         if not harness_options['is-sdk-bundled']:
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/options_defaults.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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/.
-
-def parse_options_defaults(options, preferencesBranch):
-    # this returns a unicode string
-    pref_list = []
-
-    for pref in options:
-        if ('value' in pref):
-            value = pref["value"]
-
-            if isinstance(value, float):
-                continue
-            elif isinstance(value, bool):
-                value = str(pref["value"]).lower()
-            elif isinstance(value, str): # presumably ASCII
-                value = "\"" + unicode(pref["value"]) + "\""
-            elif isinstance(value, unicode):
-                value = "\"" + pref["value"] + "\""
-            else:
-                value = str(pref["value"])
-
-            pref_list.append("pref(\"extensions." + preferencesBranch + "." + pref["name"] + "\", " + value + ");")
-
-    return "\n".join(pref_list) + "\n"
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/options_xul.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# 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/.
-
-from xml.dom.minidom import Document
-
-VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color', 'file',
-                    'directory', 'control', 'menulist', 'radio']
-
-class Error(Exception):
-    pass
-
-class BadPrefTypeError(Error):
-    pass
-
-class MissingPrefAttr(Error):
-    pass
-
-def validate_prefs(options):
-    for pref in options:
-        # Make sure there is a 'title'
-        if ("title" not in pref):
-            raise MissingPrefAttr("The '%s' pref requires a 'title'" % (pref["name"]))
-
-        # Make sure that the pref type is a valid inline pref type
-        if (pref["type"] not in VALID_PREF_TYPES):
-            raise BadPrefTypeError('%s is not a valid inline pref type' % (pref["type"]))
-
-        # Make sure the 'control' type has a 'label'
-        if (pref["type"] == "control"):
-            if ("label" not in pref):
-                raise MissingPrefAttr("The 'control' inline pref type requires a 'label'")
-
-        # Make sure the 'menulist' type has a 'menulist'
-        if (pref["type"] == "menulist" or pref["type"] == "radio"):
-            if ("options" not in pref):
-                raise MissingPrefAttr("The 'menulist' and the 'radio' inline pref types requires a 'options'")
-
-            # Make sure each option has a 'value' and a 'label'
-            for item in pref["options"]:
-                if ("value" not in item):
-                    raise MissingPrefAttr("'options' requires a 'value'")
-                if ("label" not in item):
-                    raise MissingPrefAttr("'options' requires a 'label'")
-
-        # TODO: Check that pref["type"] matches default value type
-
-def parse_options(options, jetpack_id, preferencesBranch):
-    doc = Document()
-    root = doc.createElement("vbox")
-    root.setAttribute("xmlns", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul")
-    doc.appendChild(root)
-
-    for pref in options:
-        if ("hidden" in pref and pref["hidden"] == True):
-            continue;
-
-        setting = doc.createElement("setting")
-        setting.setAttribute("pref-name", pref["name"])
-        setting.setAttribute("data-jetpack-id", jetpack_id)
-        setting.setAttribute("pref", "extensions." + preferencesBranch + "." + pref["name"])
-        setting.setAttribute("type", pref["type"])
-        setting.setAttribute("title", pref["title"])
-
-        if ("description" in pref):
-            setting.appendChild(doc.createTextNode(pref["description"]))
-
-        if (pref["type"] == "control"):
-            button = doc.createElement("button")
-            button.setAttribute("pref-name", pref["name"])
-            button.setAttribute("data-jetpack-id", jetpack_id)
-            button.setAttribute("label", pref["label"])
-            button.setAttribute("oncommand", "Services.obs.notifyObservers(null, '" +
-                                              jetpack_id + "-cmdPressed', '" +
-                                              pref["name"] + "');");
-            setting.appendChild(button)
-        elif (pref["type"] == "boolint"):
-            setting.setAttribute("on", pref["on"])
-            setting.setAttribute("off", pref["off"])
-        elif (pref["type"] == "menulist"):
-            menulist = doc.createElement("menulist")
-            menupopup = doc.createElement("menupopup")
-            for item in pref["options"]:
-                menuitem = doc.createElement("menuitem")
-                menuitem.setAttribute("value", item["value"])
-                menuitem.setAttribute("label", item["label"])
-                menupopup.appendChild(menuitem)
-            menulist.appendChild(menupopup)
-            setting.appendChild(menulist)
-        elif (pref["type"] == "radio"):
-            radiogroup = doc.createElement("radiogroup")
-            for item in pref["options"]:
-                radio = doc.createElement("radio")
-                radio.setAttribute("value", item["value"])
-                radio.setAttribute("label", item["label"])
-                radiogroup.appendChild(radio)
-            setting.appendChild(radiogroup)
-
-        root.appendChild(setting)
-
-    return doc.toprettyxml(indent="  ")
--- a/addon-sdk/source/python-lib/cuddlefish/packaging.py
+++ b/addon-sdk/source/python-lib/cuddlefish/packaging.py
@@ -18,17 +18,17 @@ env_root = os.environ.get('CUDDLEFISH_RO
 
 DEFAULT_PROGRAM_MODULE = 'main'
 
 DEFAULT_ICON = 'icon.png'
 DEFAULT_ICON64 = 'icon64.png'
 
 METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version',
                   'translators', 'contributors', 'license', 'homepage', 'icon',
-                  'icon64', 'main', 'directories', 'permissions']
+                  'icon64', 'main', 'directories', 'permissions', 'preferences']
 
 RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$')
 
 class Error(Exception):
     pass
 
 class MalformedPackageError(Error):
     pass
@@ -388,19 +388,16 @@ def generate_build_for_target(pkg_cfg, t
     if 'icon' in target_cfg:
         build['icon'] = os.path.join(target_cfg.root_dir, target_cfg.icon)
         del target_cfg['icon']
 
     if 'icon64' in target_cfg:
         build['icon64'] = os.path.join(target_cfg.root_dir, target_cfg.icon64)
         del target_cfg['icon64']
 
-    if ('preferences' in target_cfg):
-        build['preferences'] = target_cfg.preferences
-
     if 'id' in target_cfg:
         # NOTE: logic duplicated from buildJID()
         jid = target_cfg['id']
         if not ('@' in jid or jid.startswith('{')):
             jid += '@jetpack'
         build['preferencesBranch'] = jid
 
     if 'preferences-branch' in target_cfg:
--- a/addon-sdk/source/python-lib/cuddlefish/rdf.py
+++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py
@@ -145,18 +145,27 @@ def gen_manifest(template_root_dir, targ
 
     if update_url:
         manifest.set("em:updateURL", update_url)
     else:
         manifest.remove("em:updateURL")
 
     if target_cfg.get("preferences"):
         manifest.set("em:optionsType", "2")
+        
+        # workaround until bug 971249 is fixed
+        # https://bugzilla.mozilla.org/show_bug.cgi?id=971249
+        manifest.set("em:optionsURL", "data:text/xml,<placeholder/>")
+
+        # workaround for workaround, for testing simple-prefs-regression
+        if (os.path.exists(os.path.join(template_root_dir, "options.xul"))):
+            manifest.remove("em:optionsURL")
     else:
         manifest.remove("em:optionsType")
+        manifest.remove("em:optionsURL")
 
     if enable_mobile:
         target_app = dom.createElement("em:targetApplication")
         dom.documentElement.getElementsByTagName("Description")[0].appendChild(target_app)
 
         ta_desc = dom.createElement("Description")
         target_app.appendChild(ta_desc)
 
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/lib/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* 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/. */
-
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/no-prefs/lib/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* 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/. */
-
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/lib/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* 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/. */
-
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/simple-prefs/lib/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* 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/. */
-
deleted file mode 100644
--- a/addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/simple-prefs/package.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
-    "id": "jid1-fZHqN9JfrDBa8A",
-    "title": "Simple Prefs Test",
-    "author": "Erik Vold",
-    "preferences": [{
-        "name": "test",
-        "type": "bool",
-        "title": "tëst",
-        "value": false
-    },
-    {
-        "name": "test2",
-        "type": "string",
-        "title": "tëst",
-        "value": "ünicødé"
-    },
-    {
-        "name": "test3",
-        "type": "menulist",
-        "title": "\"><test",
-        "value": "1",
-        "options": [
-            {
-                "value": "0",
-                "label": "label1"
-            },
-            {
-                "value": "1",
-                "label": "label2"
-            }
-        ]
-    },
-    {
-        "name": "test4",
-        "type": "radio",
-        "title": "tëst",
-        "value": "red",
-        "options": [
-            {
-                "value": "red",
-                "label": "rouge"
-            },
-            {
-                "value": "blue",
-                "label": "bleu"
-            }
-        ]
-    }],
-    "loader": "lib/main.js"
-}
--- a/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py
@@ -15,138 +15,16 @@ from test_linker import up
 
 import xml.etree.ElementTree as ElementTree
 
 xpi_template_path = os.path.join(test_packaging.static_files_path,
                                  'xpi-template')
 
 fake_manifest = '<RDF><!-- Extension metadata is here. --></RDF>'
 
-class PrefsTests(unittest.TestCase):
-    def makexpi(self, pkg_name):
-        self.xpiname = "%s.xpi" % pkg_name
-        create_xpi(self.xpiname, pkg_name, 'preferences-files')
-        self.xpi = zipfile.ZipFile(self.xpiname, 'r')
-        options = self.xpi.read('harness-options.json')
-        self.xpi_harness_options = json.loads(options)
-
-    def setUp(self):
-        self.xpiname = None
-        self.xpi = None
-
-    def tearDown(self):
-        if self.xpi:
-            self.xpi.close()
-        if self.xpiname and os.path.exists(self.xpiname):
-            os.remove(self.xpiname)
-
-    def testPackageWithSimplePrefs(self):
-        self.makexpi('simple-prefs')
-        packageName = 'jid1-fZHqN9JfrDBa8A@jetpack'
-        self.failUnless('options.xul' in self.xpi.namelist())
-        optsxul = self.xpi.read('options.xul').decode("utf-8")
-        self.failUnlessEqual(self.xpi_harness_options["jetpackID"], packageName)
-        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], packageName)
-
-        root = ElementTree.XML(optsxul.encode('utf-8'))
-
-        xulNamespacePrefix = \
-            "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
-
-        settings = root.findall(xulNamespacePrefix + 'setting')
-
-        def assertPref(setting, name, prefType, title):
-            self.failUnlessEqual(setting.get('data-jetpack-id'), packageName)
-            self.failUnlessEqual(setting.get('pref'),
-                                 'extensions.' + packageName + '.' + name)
-            self.failUnlessEqual(setting.get('pref-name'), name)
-            self.failUnlessEqual(setting.get('type'), prefType)
-            self.failUnlessEqual(setting.get('title'), title)
-
-        assertPref(settings[0], 'test', 'bool', u't\u00EBst')
-        assertPref(settings[1], 'test2', 'string', u't\u00EBst')
-        assertPref(settings[2], 'test3', 'menulist', '"><test')
-        assertPref(settings[3], 'test4', 'radio', u't\u00EBst')
-
-        menuItems = settings[2].findall(
-            '%(0)smenulist/%(0)smenupopup/%(0)smenuitem' % { "0": xulNamespacePrefix })
-        radios = settings[3].findall(
-            '%(0)sradiogroup/%(0)sradio' % { "0": xulNamespacePrefix })
-
-        def assertOption(option, value, label):
-            self.failUnlessEqual(option.get('value'), value)
-            self.failUnlessEqual(option.get('label'), label)
-
-        assertOption(menuItems[0], "0", "label1")
-        assertOption(menuItems[1], "1", "label2")
-        assertOption(radios[0], "red", "rouge")
-        assertOption(radios[1], "blue", "bleu")
-
-        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
-        exp = [u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test", false);',
-               u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test2", "\u00FCnic\u00F8d\u00E9");',
-               u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test3", "1");',
-               u'pref("extensions.jid1-fZHqN9JfrDBa8A@jetpack.test4", "red");',
-               ]
-        self.failUnlessEqual(prefsjs, "\n".join(exp)+"\n")
-
-    def testPackageWithPreferencesBranch(self):
-        self.makexpi('preferences-branch')
-        self.failUnless('options.xul' in self.xpi.namelist())
-        optsxul = self.xpi.read('options.xul').decode("utf-8")
-        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], 
-                             "human-readable")
-
-        root = ElementTree.XML(optsxul.encode('utf-8'))
-        xulNamespacePrefix = \
-            "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
-        
-        setting = root.find(xulNamespacePrefix + 'setting')
-        self.failUnlessEqual(setting.get('pref'),
-                             'extensions.human-readable.test42')
-
-        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
-        self.failUnlessEqual(prefsjs, 
-                            'pref("extensions.human-readable.test42", true);\n')
-
-    def testPackageWithNoPrefs(self):
-        self.makexpi('no-prefs')
-        self.failIf('options.xul' in self.xpi.namelist())
-        self.failUnlessEqual(self.xpi_harness_options["jetpackID"],
-                             "jid1-fZHqN9JfrDBa8A@jetpack")
-        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
-        self.failUnlessEqual(prefsjs, "")
-
-    def testPackageWithInvalidPreferencesBranch(self):
-        self.makexpi('curly-id')
-        self.failIfEqual(self.xpi_harness_options["preferencesBranch"], 
-                         "invalid^branch*name")
-        self.failUnlessEqual(self.xpi_harness_options["preferencesBranch"], 
-                             "{34a1eae1-c20a-464f-9b0e-000000000000}")
-
-    def testPackageWithCurlyID(self):
-        self.makexpi('curly-id')
-        self.failUnlessEqual(self.xpi_harness_options["jetpackID"], 
-                             "{34a1eae1-c20a-464f-9b0e-000000000000}")
-
-        self.failUnless('options.xul' in self.xpi.namelist())
-        optsxul = self.xpi.read('options.xul').decode("utf-8")
-
-        root = ElementTree.XML(optsxul.encode('utf-8'))
-        xulNamespacePrefix = \
-            "{http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul}"
-        
-        setting = root.find(xulNamespacePrefix + 'setting')
-        self.failUnlessEqual(setting.get('pref'),
-                             'extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13')
-
-        prefsjs = self.xpi.read('defaults/preferences/prefs.js').decode("utf-8")
-        self.failUnlessEqual(prefsjs, 
-                            'pref("extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13", 26);\n')
-
 
 class Bug588119Tests(unittest.TestCase):
     def makexpi(self, pkg_name):
         self.xpiname = "%s.xpi" % pkg_name
         create_xpi(self.xpiname, pkg_name, 'bug-588119-files')
         self.xpi = zipfile.ZipFile(self.xpiname, 'r')
         options = self.xpi.read('harness-options.json')
         self.xpi_harness_options = json.loads(options)
@@ -295,17 +173,16 @@ class SmallXPI(unittest.TestCase):
         x = zipfile.ZipFile(xpi_name, "r")
         names = x.namelist()
         expected = ["components/",
                     "components/harness.js",
                     # the real template also has 'bootstrap.js', but the fake
                     # one in tests/static-files/xpi-template doesn't
                     "harness-options.json",
                     "install.rdf",
-                    "defaults/preferences/prefs.js",
                     "resources/",
                     "resources/addon-sdk/",
                     "resources/addon-sdk/lib/",
                     "resources/addon-sdk/lib/sdk/",
                     "resources/addon-sdk/lib/sdk/self.js",
                     "resources/addon-sdk/lib/sdk/core/",
                     "resources/addon-sdk/lib/sdk/util/",
                     "resources/addon-sdk/lib/sdk/net/",
--- a/addon-sdk/source/python-lib/cuddlefish/xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py
@@ -65,41 +65,16 @@ def build_xpi(template_root_dir, manifes
           for filename in goodfiles:
               abspath = os.path.join(dirpath, filename)
               arcpath = ZIPSEP.join(
                   [folder,
                    make_zipfile_path(abs_dirname, os.path.join(dirpath, filename)),
                    ])
               files_to_copy[str(arcpath)] = str(abspath)
 
-    # Handle simple-prefs
-    if 'preferences' in harness_options:
-        from options_xul import parse_options, validate_prefs
-
-        validate_prefs(harness_options["preferences"])
-
-        opts_xul = parse_options(harness_options["preferences"],
-                                 harness_options["jetpackID"],
-                                 harness_options["preferencesBranch"])
-        open('.options.xul', 'wb').write(opts_xul.encode("utf-8"))
-        zf.write('.options.xul', 'options.xul')
-        os.remove('.options.xul')
-
-        from options_defaults import parse_options_defaults
-        prefs_js = parse_options_defaults(harness_options["preferences"],
-                                          harness_options["preferencesBranch"])
-        open('.prefs.js', 'wb').write(prefs_js.encode("utf-8"))
-
-    else:
-        open('.prefs.js', 'wb').write("")
-
-    zf.write('.prefs.js', 'defaults/preferences/prefs.js')
-    os.remove('.prefs.js')
-
-
     for dirpath, dirnames, filenames in os.walk(template_root_dir):
         filenames = list(filter_filenames(filenames, IGNORED_FILES))
         dirnames[:] = filter_dirnames(dirnames)
         for dirname in dirnames:
             arcpath = make_zipfile_path(template_root_dir,
                                         os.path.join(dirpath, dirname))
             dirs_to_create.add(arcpath)
         for filename in filenames:
--- a/addon-sdk/source/test/addons/simple-prefs-regression/app-extension/install.rdf
+++ b/addon-sdk/source/test/addons/simple-prefs-regression/app-extension/install.rdf
@@ -23,11 +23,12 @@
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>Test App</em:name>
     <em:description>Harness for tests.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL></em:homepageURL>
     <em:optionsType></em:optionsType>
+    <em:optionsURL></em:optionsURL>
     <em:updateURL></em:updateURL>
   </Description>
 </RDF>
--- a/addon-sdk/source/test/addons/simple-prefs/lib/main.js
+++ b/addon-sdk/source/test/addons/simple-prefs/lib/main.js
@@ -68,16 +68,17 @@ if (app.is('Firefox')) {
             contentScriptWhen: 'end',
           	contentScript: 'function onLoad() {\n' +
                              'unsafeWindow.removeEventListener("load", onLoad, false);\n' +
                              'AddonManager.getAddonByID("' + self.id + '", function(aAddon) {\n' +
                                'unsafeWindow.gViewController.viewObjects.detail.node.addEventListener("ViewChanged", function whenViewChanges() {\n' +
                                  'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' +
                                  'setTimeout(function() {\n' + // TODO: figure out why this is necessary..
                                      'self.postMessage({\n' +
+                                       'someCount: unsafeWindow.document.querySelectorAll("setting[title=\'some-title\']").length,\n' +
                                        'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[title=\'some-title\']")),\n' +
                                        'myInteger: getAttributes(unsafeWindow.document.querySelector("setting[title=\'my-int\']")),\n' +
                                        'myHiddenInt: getAttributes(unsafeWindow.document.querySelector("setting[title=\'hidden-int\']")),\n' +
                                        'sayHello: getAttributes(unsafeWindow.document.querySelector("button[label=\'Click me!\']"))\n' +
                                      '});\n' +
                                  '}, 250);\n' +
                                '}, false);\n' +
                                'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' +
@@ -95,16 +96,19 @@ if (app.is('Firefox')) {
                            '}\n' +
                            // Wait for the load event ?
                            'if (document.readyState == "complete") {\n' +
                              'onLoad()\n' +
                            '} else {\n' +
                              'unsafeWindow.addEventListener("load", onLoad, false);\n' +
                            '}\n',
             onMessage: function(msg) {
+              // test against doc caching
+              assert.equal(msg.someCount, 1, 'there is exactly one <setting> node for somePreference');
+
               // test somePreference
               assert.equal(msg.somePreference.type, 'string', 'some pref is a string');
               assert.equal(msg.somePreference.pref, 'extensions.'+self.id+'.somePreference', 'somePreference path is correct');
               assert.equal(msg.somePreference.title, 'some-title', 'somePreference title is correct');
               assert.equal(msg.somePreference.desc, 'Some short description for the preference', 'somePreference description is correct');
               assert.equal(msg.somePreference['data-jetpack-id'], self.id, 'data-jetpack-id attribute value is correct');
 
               // test myInteger
@@ -124,15 +128,20 @@ if (app.is('Firefox')) {
               assert.equal(msg.sayHello['data-jetpack-id'], self.id, 'data-jetpack-id attribute value is correct');
 
               tab.close(done);
             }
           });
       	}
       });
   }
+
+  // run it again, to test against inline options document caching 
+  // and duplication of <setting> nodes upon re-entry to about:addons
+  exports.testAgainstDocCaching = exports.testAOM;
+
 }
 
 exports.testDefaultPreferencesBranch = function(assert) {
   assert.equal(preferencesBranch, self.id, 'preferencesBranch default the same as self.id');
 }
 
 require('sdk/test/runner').runTestsFromModule(module);
rename from addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/package.json
rename to addon-sdk/source/test/fixtures/preferences/curly-id/package.json
rename from addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/no-prefs/package.json
rename to addon-sdk/source/test/fixtures/preferences/no-prefs/package.json
rename from addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/package.json
rename to addon-sdk/source/test/fixtures/preferences/preferences-branch/package.json
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/preferences/simple-prefs/package.json
@@ -0,0 +1,74 @@
+{
+    "id": "jid1-fZHqN9JfrDBa8A",
+    "title": "Simple Prefs Test",
+    "author": "Erik Vold",
+    "preferences": [{
+        "name": "test",
+        "type": "bool",
+        "title": "tëst",
+        "value": false
+    },
+    {
+        "name": "test2",
+        "type": "string",
+        "title": "tëst",
+        "value": "ünicødé"
+    },
+    {
+        "name": "test3",
+        "type": "menulist",
+        "title": "\"><test",
+        "value": "1",
+        "options": [
+            {
+                "value": "0",
+                "label": "label1"
+            },
+            {
+                "value": "1",
+                "label": "label2"
+            }
+        ]
+    },
+    {
+        "name": "test4",
+        "type": "radio",
+        "title": "tëst",
+        "value": "red",
+        "options": [
+            {
+                "value": "red",
+                "label": "rouge"
+            },
+            {
+                "value": "blue",
+                "label": "bleu"
+            }
+        ]
+    },
+    {
+        "name": "test5",
+        "type": "boolint",
+        "title": "part part, particle",
+        "value": 7
+    },
+    {
+        "name": "test6",
+        "type": "color",
+        "title": "pop pop, popscicle",
+        "value": "#ff5e99"
+    },
+    {
+        "name": "test7",
+        "type": "file",
+        "title": "bike bike",
+        "value": "bicycle"
+    },
+    {
+        "name": "test8",
+        "type": "directory",
+        "title": "test test",
+        "value": "1-2-3"
+    }],
+    "loader": "lib/main.js"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-bootstrap.js
@@ -0,0 +1,19 @@
+/* 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/. */
+"use strict";
+
+const { Request } = require("sdk/request");
+
+exports.testBootstrapExists = function (assert, done) {
+  Request({
+    url: "resource://gre/modules/sdk/bootstrap.js",
+    onComplete: function (response) {
+      if (response.text)
+        assert.pass("the bootstrap file was found");
+      done();
+    }
+  }).get();
+};
+
+require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-native-options.js
@@ -0,0 +1,144 @@
+/* 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/. */
+"use strict";
+
+const { setDefaults, injectOptions, validate } = require('sdk/preferences/native-options');
+const { activeBrowserWindow: { document } } = require("sdk/deprecated/window-utils");
+const { preferencesBranch, id } = require('sdk/self');
+const { get } = require('sdk/preferences/service');
+const { setTimeout } = require('sdk/timers');
+const simple = require('sdk/simple-prefs');
+const fixtures = require('./fixtures');
+const { Cc, Ci } = require('chrome');
+
+exports.testValidate = function(assert) {
+  let { preferences } = packageJSON('simple-prefs');
+
+  let block = () => validate(preferences);
+
+  delete preferences[3].options[0].value;
+  assert.throws(block, /option requires both a value/, "option missing value error");
+
+  delete preferences[2].options;
+  assert.throws(block, /'test3' pref requires options/, "menulist missing options error");
+
+  preferences[1].type = 'control';
+  assert.throws(block, /'test2' control requires a label/, "control missing label error");
+
+  preferences[1].type = 'nonvalid';
+  assert.throws(block, /'test2' pref must be of valid type/, "invalid pref type error");
+
+  delete preferences[0].title;
+  assert.throws(block, /'test' pref requires a title/, "pref missing title error");
+}
+
+exports.testNoPrefs = function(assert, done) {
+  let { preferences } = packageJSON('no-prefs');
+
+  let parent = document.createDocumentFragment();
+  injectOptions(preferences || [], preferencesBranch, document, parent);
+  assert.equal(parent.children.length, 0, "No setting elements injected");
+
+  // must test with events because we can't reset default prefs
+  function onPrefChange(name) {
+    assert.fail("No preferences should be defined");
+  }
+
+  simple.on('', onPrefChange);
+  setDefaults(preferences || [], preferencesBranch);
+  setTimeout(function() {
+    assert.pass("No preferences were defined");
+    simple.off('', onPrefChange);
+    done();
+  }, 100);
+}
+
+exports.testCurlyID = function(assert) {
+  let { preferences, id } = packageJSON('curly-id');
+
+  let parent = document.createDocumentFragment();
+  injectOptions(preferences, id, document, parent);
+  assert.equal(parent.children.length, 1, "One setting elements injected");
+  assert.equal(parent.firstElementChild.attributes.pref.value, 
+               "extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13",
+               "Setting pref attribute is set properly");
+
+  setDefaults(preferences, id);
+  assert.equal(get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13'), 
+               26, "test13 is 26");
+}
+
+exports.testPreferencesBranch = function(assert) {
+  let { preferences, 'preferences-branch': prefsBranch } = packageJSON('preferences-branch');
+
+  let parent = document.createDocumentFragment();
+  injectOptions(preferences, prefsBranch, document, parent);
+  assert.equal(parent.children.length, 1, "One setting elements injected");
+  assert.equal(parent.firstElementChild.attributes.pref.value, 
+               "extensions.human-readable.test42",
+               "Setting pref attribute is set properly");
+
+  setDefaults(preferences, prefsBranch);
+  assert.equal(get('extensions.human-readable.test42'), true, "test42 is true");
+}
+
+exports.testSimplePrefs = function(assert) {
+  let { preferences } = packageJSON('simple-prefs');
+
+  function assertPref(setting, name, type, title) {
+    assert.equal(setting.getAttribute('data-jetpack-id'), id,
+                 "setting 'data-jetpack-id' attribute correct");
+    assert.equal(setting.getAttribute('pref'), 'extensions.' + id + '.' + name,
+                 "setting 'pref' attribute correct");
+    assert.equal(setting.getAttribute('pref-name'), name,
+                 "setting 'pref-name' attribute correct");
+    assert.equal(setting.getAttribute('type'), type,
+                 "setting 'type' attribute correct");
+    assert.equal(setting.getAttribute('title'), title,
+                 "setting 'title' attribute correct");
+  }
+
+  function assertOption(option, value, label) {
+    assert.equal(option.getAttribute('value'), value, "value attribute correct");
+    assert.equal(option.getAttribute('label'), label, "label attribute correct");
+  }
+
+  let parent = document.createDocumentFragment();
+  injectOptions(preferences, preferencesBranch, document, parent);
+  assert.equal(parent.children.length, 8, "Eight setting elements injected");
+
+  assertPref(parent.children[0], 'test', 'bool', 't\u00EBst');
+  assertPref(parent.children[1], 'test2', 'string', 't\u00EBst');
+  assertPref(parent.children[2], 'test3', 'menulist', '"><test');
+  assertPref(parent.children[3], 'test4', 'radio', 't\u00EBst');
+
+  assertPref(parent.children[4], 'test5', 'boolint', 'part part, particle');
+  assertPref(parent.children[5], 'test6', 'color', 'pop pop, popscicle');
+  assertPref(parent.children[6], 'test7', 'file', 'bike bike');
+  assertPref(parent.children[7], 'test8', 'directory', 'test test');
+
+  let menuItems = parent.children[2].querySelectorAll('menupopup>menuitem');
+  let radios = parent.children[3].querySelectorAll('radiogroup>radio');
+
+  assertOption(menuItems[0], '0', 'label1');
+  assertOption(menuItems[1], '1', 'label2');
+  assertOption(radios[0], 'red', 'rouge');
+  assertOption(radios[1], 'blue', 'bleu');
+
+  setDefaults(preferences, preferencesBranch);
+  assert.strictEqual(simple.prefs.test, false, "test is false");
+  assert.strictEqual(simple.prefs.test2, "\u00FCnic\u00F8d\u00E9", "test2 is unicode"); 
+  assert.strictEqual(simple.prefs.test3, "1", "test3 is '1'");
+  assert.strictEqual(simple.prefs.test4, "red", "test4 is 'red'");
+
+  // default pref branch can't be "reset", bug 1012231
+  Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService).
+    getDefaultBranch('extensions.' + preferencesBranch).deleteBranch('');
+}
+
+function packageJSON(dir) {
+  return require(fixtures.url('preferences/' + dir + '/package.json'));
+}
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -127,26 +127,71 @@ exports.testPageModIncludes = function(a
   testPageMod(assert, done, testPageURI, [
       createPageModTest("*", false),
       createPageModTest("*.google.com", false),
       createPageModTest("resource:*", true),
       createPageModTest("resource:", false),
       createPageModTest(testPageURI, true)
     ],
     function (win, done) {
-      waitUntil(function () win.localStorage[testPageURI],
-                     testPageURI + " page-mod to be executed")
-          .then(function () {
-            asserts.forEach(function(fn) {
-              fn(assert, win);
-            });
-            done();
-          });
-    }
-    );
+      waitUntil(() => win.localStorage[testPageURI],
+          testPageURI + " page-mod to be executed")
+        .then(() => {
+          asserts.forEach(fn => fn(assert, win));
+          win.localStorage.clear();
+          done();
+        });
+    });
+};
+
+exports.testPageModExcludes = function(assert, done) {
+  var asserts = [];
+  function createPageModTest(include, exclude, expectedMatch) {
+    // Create an 'onload' test function...
+    asserts.push(function(test, win) {
+      var matches = JSON.stringify([include, exclude]) in win.localStorage;
+      assert.ok(expectedMatch ? matches : !matches,
+          "[include, exclude] = [" + include + ", " + exclude +
+          "] match test, expected: " + expectedMatch);
+    });
+    // ...and corresponding PageMod options
+    return {
+      include: include,
+      exclude: exclude,
+      contentScript: 'new ' + function() {
+        self.on("message", function(msg) {
+          // The key in localStorage is "[<include>, <exclude>]".
+          window.localStorage[JSON.stringify(msg)] = true;
+        });
+      },
+      // The testPageMod callback with test assertions is called on 'end',
+      // and we want this page mod to be attached before it gets called,
+      // so we attach it on 'start'.
+      contentScriptWhen: 'start',
+      onAttach: function(worker) {
+        worker.postMessage([this.include[0], this.exclude[0]]);
+      }
+    };
+  }
+
+  testPageMod(assert, done, testPageURI, [
+      createPageModTest("*", testPageURI, false),
+      createPageModTest(testPageURI, testPageURI, false),
+      createPageModTest(testPageURI, "resource://*", false),
+      createPageModTest(testPageURI, "*.google.com", true)
+    ],
+    function (win, done) {
+      waitUntil(() => win.localStorage[JSON.stringify([testPageURI, "*.google.com"])],
+          testPageURI + " page-mod to be executed")
+        .then(() => {
+          asserts.forEach(fn => fn(assert, win));
+          win.localStorage.clear();
+          done();
+        });
+    });
 };
 
 exports.testPageModValidationAttachTo = function(assert) {
   [{ val: 'top', type: 'string "top"' },
    { val: 'frame', type: 'string "frame"' },
    { val: ['top', 'existing'], type: 'array with "top" and "existing"' },
    { val: ['frame', 'existing'], type: 'array with "frame" and "existing"' },
    { val: ['top'], type: 'array with "top"' },
@@ -182,16 +227,37 @@ exports.testPageModValidationInclude = f
   [{ val: '*.validation111', type: 'string' },
    { val: /validation111/, type: 'regexp' },
    { val: ['*.validation111'], type: 'array with length > 0'}].forEach((include) => {
     new PageMod({ include: include.val });
     assert.pass("PageMod() does not throw when include option is " + include.type);
   });
 };
 
+exports.testPageModValidationExclude = function(assert) {
+  let includeVal = '*.validation111';
+
+  [{ val: {}, type: 'object' },
+   { val: [], type: 'empty array'},
+   { val: [/regexp/, 1], type: 'array with non string/regexp' },
+   { val: 1, type: 'number' }].forEach((exclude) => {
+    assert.throws(() => new PageMod({ include: includeVal, exclude: exclude.val }),
+      /If set, the `exclude` option must always contain at least one rule as a string, regular expression, or an array of strings and regular expressions./,
+      "PageMod() throws when 'exclude' option is " + exclude.type + ".");
+  });
+
+  [{ val: undefined, type: 'undefined' },
+   { val: '*.validation111', type: 'string' },
+   { val: /validation111/, type: 'regexp' },
+   { val: ['*.validation111'], type: 'array with length > 0'}].forEach((exclude) => {
+    new PageMod({ include: includeVal, exclude: exclude.val });
+    assert.pass("PageMod() does not throw when exclude option is " + exclude.type);
+  });
+};
+
 /* Tests for internal functions. */
 exports.testCommunication1 = function(assert, done) {
   let workerDone = false,
       callbackDone = null;
 
   testPageMod(assert, done, "about:", [{
       include: "about:*",
       contentScriptWhen: 'end',
--- a/addon-sdk/source/test/test-simple-prefs.js
+++ b/addon-sdk/source/test/test-simple-prefs.js
@@ -33,71 +33,76 @@ exports.testIterations = function(assert
   }
   assert.equal([].toString(), prefAry.toString(), "for (x in y) part 2/2 works");
 }
 
 exports.testSetGetBool = function(assert) {
   assert.equal(sp.test, undefined, "Value should not exist");
   sp.test = true;
   assert.ok(sp.test, "Value read should be the value previously set");
+  delete sp.test;
 };
 
 // TEST: setting and getting preferences with special characters work
 exports.testSpecialChars = function(assert, done) {
   let chars = specialChars.split("");
   let len = chars.length;
 
   let count = 0;
   chars.forEach(function(char) {
     let rand = Math.random() + "";
     simplePrefs.on(char, function onPrefChanged() {
       simplePrefs.removeListener(char, onPrefChanged);
       assert.equal(sp[char], rand, "setting pref with a name that is a special char, " + char + ", worked!");
+      delete sp[char];
 
       // end test
       if (++count == len)
         done();
     })
     sp[char] = rand;
   });
 };
 
 exports.testSetGetInt = function(assert) {
   assert.equal(sp["test-int"], undefined, "Value should not exist");
   sp["test-int"] = 1;
   assert.equal(sp["test-int"], 1, "Value read should be the value previously set");
+  delete sp["test-int"];
 };
 
 exports.testSetComplex = function(assert) {
   try {
     sp["test-complex"] = {test: true};
     assert.fail("Complex values are not allowed");
   }
   catch (e) {
     assert.pass("Complex values are not allowed");
   }
 };
 
 exports.testSetGetString = function(assert) {
   assert.equal(sp["test-string"], undefined, "Value should not exist");
   sp["test-string"] = "test";
   assert.equal(sp["test-string"], "test", "Value read should be the value previously set");
+  delete sp["test-string"];
 };
 
 exports.testHasAndRemove = function(assert) {
   sp.test = true;
   assert.ok(("test" in sp), "Value exists");
   delete sp.test;
   assert.equal(sp.test, undefined, "Value should be undefined");
 };
 
 exports.testPrefListener = function(assert, done) {
   let listener = function(prefName) {
     simplePrefs.removeListener('test-listener', listener);
     assert.equal(prefName, "test-listen", "The prefs listener heard the right event");
+    delete sp["test-listen"];
     done();
   };
 
   simplePrefs.on("test-listen", listener);
 
   sp["test-listen"] = true;
 
   // Wildcard listen
@@ -107,23 +112,24 @@ exports.testPrefListener = function(asse
   let wildlistener = function(prefName) {
     if (toSet.indexOf(prefName) > -1) observed.push(prefName);
   };
 
   simplePrefs.on('',wildlistener);
 
   toSet.forEach(function(pref) {
     sp[pref] = true;
+    delete sp[pref];
   });
 
-  assert.ok((observed.length == 3 && toSet.length == 3),
+  assert.ok((observed.length === 6 && toSet.length === 3),
       "Wildcard lengths inconsistent" + JSON.stringify([observed.length, toSet.length]));
 
   toSet.forEach(function(pref,ii) {
-    assert.equal(observed[ii], pref, "Wildcard observed " + pref);
+    assert.equal(observed[2*ii], pref, "Wildcard observed " + pref);
   });
 
   simplePrefs.removeListener('',wildlistener);
 
 };
 
 exports.testBtnListener = function(assert, done) {
   let name = "test-btn-listen";
@@ -145,16 +151,17 @@ exports.testPrefRemoveListener = functio
       assert.fail("The prefs listener was not removed");
 
     simplePrefs.removeListener("test-listen2", listener);
 
     sp["test-listen2"] = false;
 
     setTimeout(function() {
       assert.pass("The prefs listener was removed");
+      delete sp["test-listen2"];
       done();
     }, 250);
   };
 
   simplePrefs.on("test-listen2", listener);
 
   // emit change
   sp["test-listen2"] = true;
@@ -171,16 +178,17 @@ exports.testPrefUnloadListener = functio
 
     loader.unload();
 
     // this may not execute after unload, but definitely shouldn't fire listener
     sp.prefs["test-listen3"] = false;
     // this should execute, but also definitely shouldn't fire listener
     require("sdk/simple-prefs").prefs["test-listen3"] = false;
 
+    delete sp.prefs["test-listen3"];
     done();
   };
 
   sp.on("test-listen3", listener);
 
   // emit change
   sp.prefs["test-listen3"] = true;
 };
@@ -198,16 +206,17 @@ exports.testPrefUnloadWildcardListener =
 
     loader.unload();
 
     // this may not execute after unload, but definitely shouldn't fire listener
     sp.prefs[testpref] = false;
     // this should execute, but also definitely shouldn't fire listener
     require("sdk/simple-prefs").prefs[testpref] = false;
 
+    delete sp.prefs[testpref];
     done();
   };
 
   sp.on("", listener);
   // emit change
   sp.prefs[testpref] = true;
 };
 
--- a/b2g/components/B2GComponents.manifest
+++ b/b2g/components/B2GComponents.manifest
@@ -24,21 +24,16 @@ category xpcom-directory-providers brows
 component {3a54788b-48cc-4ab4-93d6-0d6a8ef74f8e} ActivitiesGlue.js
 contract @mozilla.org/dom/activities/ui-glue;1 {3a54788b-48cc-4ab4-93d6-0d6a8ef74f8e}
 
 # ProcessGlobal.js
 component {1a94c87a-5ece-4d11-91e1-d29c29f21b28} ProcessGlobal.js
 contract @mozilla.org/b2g-process-global;1 {1a94c87a-5ece-4d11-91e1-d29c29f21b28}
 category app-startup ProcessGlobal service,@mozilla.org/b2g-process-global;1
 
-# ContentHandler.js
-component {d18d0216-d50c-11e1-ba54-efb18d0ef0ac} ContentHandler.js
-contract @mozilla.org/b2g/activities-content-handler;1 {d18d0216-d50c-11e1-ba54-efb18d0ef0ac}
-category app-startup ContentHandler service,@mozilla.org/b2g/activities-content-handler;1
-
 # PaymentGlue.js
 component {8b83eabc-7929-47f4-8b48-4dea8d887e4b} PaymentGlue.js
 contract @mozilla.org/payment/ui-glue;1 {8b83eabc-7929-47f4-8b48-4dea8d887e4b}
 
 # TelProtocolHandler.js
 component {782775dd-7351-45ea-aff1-0ffa872cfdd2} TelProtocolHandler.js
 contract @mozilla.org/network/protocol;1?name=tel {782775dd-7351-45ea-aff1-0ffa872cfdd2}
 
deleted file mode 100644
--- a/b2g/components/ContentHandler.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-
-"use strict";
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "cpmm", function() {
-  return Cc["@mozilla.org/childprocessmessagemanager;1"]
-           .getService(Ci.nsIMessageSender);
-});
-
-function debug(aMsg) {
-  //dump("--*-- ContentHandler: " + aMsg + "\n");
-}
-
-const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
-
-let ActivityContentFactory = {
-  createInstance: function createInstance(outer, iid) {
-    if (outer != null) {
-      throw Cr.NS_ERROR_NO_AGGREGATION;
-    }
-    return new ActivityContentHandler().QueryInterface(iid);
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
-}
-
-function ActivityContentHandler() {
-}
-
-ActivityContentHandler.prototype = {
-  handleContent: function handleContent(aMimetype, aContext, aRequest) {
-    if (!(aRequest instanceof Ci.nsIChannel))
-      throw NS_ERROR_WONT_HANDLE_CONTENT;
-
-    let detail = {
-      "type": aMimetype,
-      "url": aRequest.URI.spec
-    };
-    cpmm.sendAsyncMessage("content-handler", detail);
-
-    aRequest.cancel(Cr.NS_BINDING_ABORTED);
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler])
-}
-
-function ContentHandler() {
-  this.classIdMap = {};
-}
-
-ContentHandler.prototype = {
-  observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "app-startup") {
-      // We only want to register these from content processes.
-      let appInfo = Cc["@mozilla.org/xre/app-info;1"];
-      if (appInfo.getService(Ci.nsIXULRuntime)
-          .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
-        return;
-      }
-    }
-
-    cpmm.addMessageListener("Activities:RegisterContentTypes", this);
-    cpmm.addMessageListener("Activities:UnregisterContentTypes", this);
-    cpmm.sendAsyncMessage("Activities:GetContentTypes", { });
-  },
-
-  /**
-    * Do the component registration for a content type.
-    * We only need to register one component per content type, even if several
-    * apps provide it, so we keep track of the number of providers for each
-    * content type.
-    */
-  registerContentHandler: function registerContentHandler(aContentType) {
-    debug("Registering " + aContentType);
-
-    // We already have a provider for this content type, just increase the
-    // tracking count.
-    if (this.classIdMap[aContentType]) {
-      this.classIdMap[aContentType].count++;
-      return;
-    }
-
-    let contractID = "@mozilla.org/uriloader/content-handler;1?type=" +
-                     aContentType;
-    let uuidGen = Cc["@mozilla.org/uuid-generator;1"]
-                    .getService(Ci.nsIUUIDGenerator);
-    let id = Components.ID(uuidGen.generateUUID().toString());
-    this.classIdMap[aContentType] = { count: 1, id: id };
-    let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-    cr.registerFactory(Components.ID(id), "Activity Content Handler", contractID,
-                       ActivityContentFactory);
-  },
-
-  /**
-    * Do the component unregistration for a content type.
-    */
-  unregisterContentHandler: function registerContentHandler(aContentType) {
-    debug("Unregistering " + aContentType);
-
-    let record = this.classIdMap[aContentType];
-    if (!record) {
-      return;
-    }
-
-    // Bail out if we still have providers left for this content type.
-    if (--record.count > 0) {
-      return;
-    }
-
-    let contractID = "@mozilla.org/uriloader/content-handler;1?type=" +
-                     aContentType;
-    let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-    cr.unregisterFactory(record.id, ActivityContentFactory);
-    delete this.classIdMap[aContentType]
-  },
-
-  receiveMessage: function(aMessage) {
-    let data = aMessage.data;
-
-    switch (aMessage.name) {
-      case "Activities:RegisterContentTypes":
-        data.contentTypes.forEach(this.registerContentHandler, this);
-        break;
-      case "Activities:UnregisterContentTypes":
-        data.contentTypes.forEach(this.unregisterContentHandler, this);
-        break;
-    }
-  },
-
-  classID: Components.ID("{d18d0216-d50c-11e1-ba54-efb18d0ef0ac}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler,
-                                         Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference])
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentHandler]);
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -5,17 +5,16 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += ['test']
 
 EXTRA_COMPONENTS += [
     'ActivitiesGlue.js',
     'AlertsService.js',
     'B2GAboutRedirector.js',
-    'ContentHandler.js',
     'ContentPermissionPrompt.js',
     'FilePicker.js',
     'HelperAppDialog.js',
     'MailtoProtocolHandler.js',
     'PaymentGlue.js',
     'ProcessGlobal.js',
     'SmsProtocolHandler.js',
     'TelProtocolHandler.js',
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="ca283b9db2b151d465cfd2e19346cf58fe89e413"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="e5f4683183a1dec2cfdb21b76509819977e9d09c"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="65fba428f8d76336b33ddd9e15900357953600ba">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <!-- Stock Android things -->
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="ca283b9db2b151d465cfd2e19346cf58fe89e413"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="e5f4683183a1dec2cfdb21b76509819977e9d09c"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -13,17 +13,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "2f15fe97023bf41e29660a2d427bd43a3ff181b0", 
+    "revision": "9b29b2b76fa9038d3162261c174a92dd5ef704d2", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="c462d9183d294a2d8ecc472f593ea8cfa15bc9de"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7c55cc2baabc2d66d512768e79b9cbc67bb83040"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="996b5c6a2fd2b8a0124c0eab80eb72a4daece7bc"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8a4baf82a131a7853cf7e7f9cf74253927b2f355"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -30,16 +30,22 @@
                 <menuitem id="menu_newRemoteWindow"
                           label="New e10s Window"
                           hidden="true"
                           command="Tools:RemoteWindow"/>
                 <menuitem id="menu_newNonRemoteWindow"
                           label="New Non-e10s Window"
                           hidden="true"
                           command="Tools:NonRemoteWindow"/>
+#ifdef MAC_NON_BROWSER_WINDOW
+                <menuitem id="menu_openLocation"
+                          label="&openLocationCmd.label;"
+                          command="Browser:OpenLocation"
+                          key="focusURLBar"/>
+#endif
                 <menuitem id="menu_openFile"
                           label="&openFileCmd.label;"
                           command="Browser:OpenFile"
                           key="openFileKb"
                           accesskey="&openFileCmd.accesskey;"/>
 #ifdef HAVE_SHELL_SERVICE
 #ifdef XP_WIN
 #ifdef MOZ_METRO
--- a/browser/base/content/macBrowserOverlay.xul
+++ b/browser/base/content/macBrowserOverlay.xul
@@ -1,15 +1,17 @@
 <?xml version="1.0"?>
 # -*- Mode: HTML -*-
 #
 # 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/.
 
+#define MAC_NON_BROWSER_WINDOW
+
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 <?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
 <?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
 
 # All DTD information is stored in a separate file so that it can be shared by
--- a/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js
+++ b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js
@@ -36,24 +36,26 @@ function bookmarksMenuPanelShown() {
  *
  * @param aItemWithContextMenu the item that we need to synthesize hte
  *        right click on in order to open the context menu.
  */
 function checkPlacesContextMenu(aItemWithContextMenu) {
   return Task.spawn(function* () {
     let contextMenu = document.getElementById("placesContext");
     let newBookmarkItem = document.getElementById("placesContext_new:bookmark");
+    info("Waiting for context menu on " + aItemWithContextMenu.id);
     let shownPromise = popupShown(contextMenu);
     EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu,
                                        {type: "contextmenu", button: 2});
     yield shownPromise;
 
     ok(!newBookmarkItem.hasAttribute("disabled"),
        "New bookmark item shouldn't be disabled");
 
+    info("Closing context menu");
     yield closePopup(contextMenu);
   });
 }
 
 /**
  * Opens the bookmarks menu panel, and then opens each of the "special"
  * submenus in that list. Then it checks that those submenu's context menus
  * are properly hooked up to a controller.
@@ -76,24 +78,27 @@ function checkSpecialContextMenus() {
                                                              "anonid", "dropmarker");
     EventUtils.synthesizeMouseAtCenter(dropmarker, {});
     info("Waiting for bookmarks menu popup to show after clicking dropmarker.")
     yield shownPromise;
 
     for (let menuID in kSpecialItemIDs) {
       let menuItem = document.getElementById(menuID);
       let menuPopup = document.getElementById(kSpecialItemIDs[menuID]);
+      info("Waiting to open menu for " + menuID);
       let shownPromise = popupShown(menuPopup);
       EventUtils.synthesizeMouseAtCenter(menuItem, {});
       yield shownPromise;
 
       yield checkPlacesContextMenu(menuPopup);
+      info("Closing menu for " + menuID);
       yield closePopup(menuPopup);
     }
 
+    info("Closing bookmarks menu");
     yield closePopup(bookmarksMenuPopup);
   });
 }
 
 /**
  * Closes a focused popup by simulating pressing the Escape key,
  * and returns a Promise that resolves as soon as the popup is closed.
  *
@@ -111,44 +116,48 @@ function closePopup(aPopup) {
  * to the controller of a view.
  */
 function checkBookmarksItemsChevronContextMenu() {
   return Task.spawn(function*() {
     let chevronPopup = document.getElementById("PlacesChevronPopup");
     let shownPromise = popupShown(chevronPopup);
     let chevron = document.getElementById("PlacesChevron");
     EventUtils.synthesizeMouseAtCenter(chevron, {});
+    info("Waiting for bookmark toolbar item chevron popup to show");
     yield shownPromise;
     yield waitForCondition(() => {
       for (let child of chevronPopup.children) {
         if (child.style.visibility != "hidden")
           return true;
       }
     });
     yield checkPlacesContextMenu(chevronPopup);
+    info("Waiting for bookmark toolbar item chevron popup to close");
     yield closePopup(chevronPopup);
   });
 }
 
 /**
  * Forces the window to a width that causes the nav-bar to overflow
  * its contents. Returns a Promise that resolves as soon as the
  * overflowable nav-bar is showing its chevron.
  */
 function overflowEverything() {
+  info("Waiting for overflow");
   window.resizeTo(kSmallWidth, window.outerHeight);
   return waitForCondition(() => gNavBar.hasAttribute("overflowing"));
 }
 
 /**
  * Returns the window to its original size from the start of the test,
  * and returns a Promise that resolves when the nav-bar is no longer
  * overflowing.
  */
 function stopOverflowing() {
+  info("Waiting until we stop overflowing");
   window.resizeTo(kOriginalWindowWidth, window.outerHeight);
   return waitForCondition(() => !gNavBar.hasAttribute("overflowing"));
 }
 
 /**
  * Checks that an item with ID aID is overflowing in the nav-bar.
  *
  * @param aID the ID of the node to check for overflowingness.
@@ -197,16 +206,17 @@ add_task(function* testOverflowingBookma
   yield checkSpecialContextMenus();
 });
 
 /**
  * Test that the bookmarks toolbar items context menu still works if moved
  * to the menu from the overflow panel, and then back to the toolbar.
  */
 add_task(function* testOverflowingBookmarksItemsContextMenu() {
+  info("Ensuring panel is ready.");
   yield PanelUI.ensureReady();
 
   let bookmarksToolbarItems = document.getElementById(kBookmarksItems);
   gCustomizeMode.addToToolbar(bookmarksToolbarItems);
   yield checkPlacesContextMenu(bookmarksToolbarItems);
 
   yield overflowEverything();
   checkOverflowing(kBookmarksItems)
@@ -230,16 +240,17 @@ add_task(function* testOverflowingBookma
   gCustomizeMode.addToToolbar(bookmarksToolbarItems);
 
   // We make the PlacesToolbarItems element be super tiny in order to force
   // the bookmarks toolbar items into overflowing and making the chevron
   // show itself.
   let placesToolbarItems = document.getElementById("PlacesToolbarItems");
   let placesChevron = document.getElementById("PlacesChevron");
   placesToolbarItems.style.maxWidth = "10px";
+  info("Waiting for chevron to no longer be collapsed");
   yield waitForCondition(() => !placesChevron.collapsed);
 
   yield checkBookmarksItemsChevronContextMenu();
 
   yield overflowEverything();
   checkOverflowing(kBookmarksItems);
 
   yield stopOverflowing();
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Advanced panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/advanced.js"/>
 
 <preferences id="advancedPreferences">
   <preference id="browser.preferences.advanced.selectedTabIndex"
               name="browser.preferences.advanced.selectedTabIndex"
               type="int"/>
--- a/browser/components/preferences/in-content/applications.xul
+++ b/browser/components/preferences/in-content/applications.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Applications panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/applications.js"/>
 
 <preferences id="feedsPreferences">
   <preference id="browser.feeds.handler"
               name="browser.feeds.handler"
               type="string"/>
--- a/browser/components/preferences/in-content/content.xul
+++ b/browser/components/preferences/in-content/content.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Content panel -->
 
 <preferences id="contentPreferences">
 
   <!-- Popups -->
   <preference id="dom.disable_open_during_load"
               name="dom.disable_open_during_load"
               type="bool"/>
 
--- a/browser/components/preferences/in-content/jar.mn
+++ b/browser/components/preferences/in-content/jar.mn
@@ -1,21 +1,14 @@
 # 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/.
 
 browser.jar:
    content/browser/preferences/in-content/preferences.js
 *  content/browser/preferences/in-content/preferences.xul
-*  content/browser/preferences/in-content/main.xul
 *  content/browser/preferences/in-content/main.js
-   content/browser/preferences/in-content/privacy.xul
 *  content/browser/preferences/in-content/privacy.js
-*  content/browser/preferences/in-content/advanced.xul
 *  content/browser/preferences/in-content/advanced.js
-   content/browser/preferences/in-content/applications.xul
 *  content/browser/preferences/in-content/applications.js
-   content/browser/preferences/in-content/content.xul
    content/browser/preferences/in-content/content.js
-   content/browser/preferences/in-content/sync.xul
    content/browser/preferences/in-content/sync.js
-   content/browser/preferences/in-content/security.xul
    content/browser/preferences/in-content/security.js
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- General panel -->
 
 <script type="application/javascript" 
         src="chrome://browser/content/preferences/in-content/main.js"/>
 
 <preferences id="mainPreferences">
 
     <!-- Startup -->
     <preference id="browser.startup.page"
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -27,22 +27,16 @@ function init_all() {
   gSyncPane.init();
   gSecurityPane.init();
   var initFinished = document.createEvent("Event");
   initFinished.initEvent("Initialized", true, true);
   document.dispatchEvent(initFinished);
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
-
-  if (history.length > 1 && history.state) {
-    selectCategory(history.state);
-  } else {
-    history.replaceState("paneGeneral", document.title);
-  }
 }
 
 function selectCategory(name) {
   let categories = document.getElementById("categories");
   let item = categories.querySelector(".category[value=" + name + "]");
   categories.selectedItem = item;
   gotoPref(name);
 }
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Privacy panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/privacy.js"/>
 
 <preferences id="privacyPreferences">
 
   <!-- Tracking -->
   <preference id="privacy.donottrackheader.enabled"
--- a/browser/components/preferences/in-content/security.xul
+++ b/browser/components/preferences/in-content/security.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Security panel -->
 
 <script type="application/javascript" 
         src="chrome://browser/content/preferences/in-content/security.js"/>  
 
 <preferences id="securityPreferences">
   <!-- XXX buttons -->
   <preference id="pref.privacy.disable_button.view_passwords"
               name="pref.privacy.disable_button.view_passwords"
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -1,11 +1,13 @@
-<!-- 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/.  -->
+# 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/.
+
+<!-- Synch panel -->
 
 <preferences>
   <preference id="engine.addons"
               name="services.sync.engine.addons"
               type="bool"/>
   <preference id="engine.bookmarks"
               name="services.sync.engine.bookmarks"
               type="bool"/>
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -2129,28 +2129,32 @@ let SessionStoreInternal = {
       ix = -1;
 
     let session = {
       lastUpdate: Date.now(),
       startTime: this._sessionStartTime,
       recentCrashes: this._recentCrashes
     };
 
-    // get open Scratchpad window states too
-    let scratchpads = ScratchpadManager.getSessionState();
-
     let state = {
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
       session: session,
-      scratchpads: scratchpads,
       global: this._globalState.getState()
     };
 
+    if (Cu.isModuleLoaded("resource:///modules/devtools/scratchpad-manager.jsm")) {
+      // get open Scratchpad window states too
+      let scratchpads = ScratchpadManager.getSessionState();
+      if (scratchpads && scratchpads.length) {
+        state.scratchpads = scratchpads;
+      }
+    }
+
     // Persist the last session if we deferred restoring it
     if (LastSession.canRestore) {
       state.lastSessionState = LastSession.getState();
     }
 
     // If we were called by the SessionSaver and started with only a private
     // window we want to pass the deferred initial state to not lose the
     // previous session.
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -20,17 +20,17 @@
     </resources>
     <content>
       <xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type">
         <xul:hbox anonid="details" align="center" flex="1">
           <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
           <xul:deck anonid="translationStates" selectedIndex="0">
 
             <!-- offer to translate -->
-            <xul:hbox class="translate-offer-box" align="baseline">
+            <xul:hbox class="translate-offer-box" align="center">
               <xul:label value="&translation.thisPageIsIn.label;"/>
               <xul:menulist anonid="detectedLanguage">
                 <xul:menupopup/>
               </xul:menulist>
               <xul:label value="&translation.translateThisPage.label;"/>
               <xul:button label="&translation.translate.button;" anonid="translate"
                           oncommand="document.getBindingParent(this).translate();"/>
               <xul:button label="&translation.notNow.button;" anonid="notNow"
@@ -38,17 +38,17 @@
             </xul:hbox>
 
             <!-- translating -->
             <xul:vbox class="translating-box" pack="center">
               <xul:label value="&translation.translatingContent.label;"/>
             </xul:vbox>
 
             <!-- translated -->
-            <xul:hbox class="translated-box" align="baseline">
+            <xul:hbox class="translated-box" align="center">
               <xul:label value="&translation.translatedFrom.label;"/>
               <xul:menulist anonid="fromLanguage"
                             oncommand="document.getBindingParent(this).translate()">
                 <xul:menupopup/>
               </xul:menulist>
               <xul:label value="&translation.translatedTo.label;"/>
               <xul:menulist anonid="toLanguage"
                             oncommand="document.getBindingParent(this).translate()">
@@ -59,17 +59,17 @@
                           label="&translation.showOriginal.button;"
                           oncommand="document.getBindingParent(this).showOriginal();"/>
               <xul:button anonid="showTranslation"
                           label="&translation.showTranslation.button;"
                           oncommand="document.getBindingParent(this).showTranslation();"/>
             </xul:hbox>
 
             <!-- error -->
-            <xul:hbox class="translation-error" align="baseline">
+            <xul:hbox class="translation-error" align="center">
               <xul:label value="&translation.errorTranslating.label;"/>
               <xul:button label="&translation.tryAgain.button;" anonid="tryAgain"
                           oncommand="document.getBindingParent(this).translate();"/>
             </xul:hbox>
 
           </xul:deck>
           <xul:spacer flex="1"/>
 
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -39,19 +39,22 @@ const EVENTS = {
   FETCHED_PROPERTIES: "Debugger:FetchedProperties",
   FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
   FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
 
   // When a breakpoint has been added or removed on the debugger server.
   BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
   BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
 
-  // When a breakpoint has been shown or hidden in the source editor.
-  BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
-  BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
+  // When a breakpoint has been shown or hidden in the source editor
+  // or the pane.
+  BREAKPOINT_SHOWN_IN_EDITOR: "Debugger:BreakpointShownInEditor",
+  BREAKPOINT_SHOWN_IN_PANE: "Debugger:BreakpointShownInPane",
+  BREAKPOINT_HIDDEN_IN_EDITOR: "Debugger:BreakpointHiddenInEditor",
+  BREAKPOINT_HIDDEN_IN_PANE: "Debugger:BreakpointHiddenInPane",
 
   // When a conditional breakpoint's popup is showing or hiding.
   CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
   CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding",
 
   // When event listeners are fetched or event breakpoints are updated.
   EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
   EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",
@@ -63,16 +66,19 @@ const EVENTS = {
   // When a function search was performed.
   FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
   FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
 
   // When a global text search was performed.
   GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
   GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
 
+  // After the the StackFrames object has been filled with frames
+  AFTER_FRAMES_REFILLED: "Debugger:AfterFramesRefilled",
+
   // After the stackframes are cleared and debugger won't pause anymore.
   AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
 
   // When the options popup is showing or hiding.
   OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
   OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
 
   // When the widgets layout has been changed.
@@ -85,31 +91,33 @@ const FRAME_TYPE = {
   CONDITIONAL_BREAKPOINT_EVAL: 1,
   WATCH_EXPRESSIONS_EVAL: 2,
   PUBLIC_CLIENT_EVAL: 3
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/event-emitter.js");
-Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const promise = require("devtools/toolkit/deprecated-sync-thenables");
 const Editor = require("devtools/sourceeditor/editor");
 const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const FastListWidget = require("devtools/shared/widgets/FastListWidget");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
   "resource://gre/modules/devtools/DevToolsUtils.jsm");
@@ -142,96 +150,88 @@ let DebuggerController = {
   },
 
   /**
    * Initializes the view.
    *
    * @return object
    *         A promise that is resolved when the debugger finishes startup.
    */
-  startupDebugger: function() {
+  startupDebugger: Task.async(function*() {
     if (this._startup) {
-      return this._startup;
+      return;
     }
 
-    return this._startup = DebuggerView.initialize();
-  },
+    yield DebuggerView.initialize();
+    this._startup = true;
+  }),
 
   /**
    * Destroys the view and disconnects the debugger client from the server.
    *
    * @return object
    *         A promise that is resolved when the debugger finishes shutdown.
    */
-  shutdownDebugger: function() {
+  shutdownDebugger: Task.async(function*() {
     if (this._shutdown) {
-      return this._shutdown;
+      return;
     }
 
-    return this._shutdown = DebuggerView.destroy().then(() => {
-      DebuggerView.destroy();
-      this.SourceScripts.disconnect();
-      this.StackFrames.disconnect();
-      this.ThreadState.disconnect();
-      this.Tracer.disconnect();
-      this.disconnect();
-    });
-  },
+    yield DebuggerView.destroy();
+    this.SourceScripts.disconnect();
+    this.StackFrames.disconnect();
+    this.ThreadState.disconnect();
+    this.Tracer.disconnect();
+    this.disconnect();
+
+    this._shutdown = true;
+  }),
 
   /**
    * Initiates remote debugging based on the current target, wiring event
    * handlers as necessary.
    *
    * @return object
    *         A promise that is resolved when the debugger finishes connecting.
    */
-  connect: function() {
-    if (this._connection) {
-      return this._connection;
+  connect: Task.async(function*() {
+    if (this._connected) {
+      return;
     }
 
-    let startedDebugging = promise.defer();
-    this._connection = startedDebugging.promise;
-
     let target = this._target;
     let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
     target.on("close", this._onTabDetached);
     target.on("navigate", this._onTabNavigated);
     target.on("will-navigate", this._onTabNavigated);
     this.client = client;
 
     if (addonActor) {
-      this._startAddonDebugging(addonActor, startedDebugging.resolve);
+      yield this._startAddonDebugging(addonActor);
     } else if (target.chrome) {
-      this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
+      yield this._startChromeDebugging(chromeDebugger);
     } else {
-      this._startDebuggingTab(startedDebugging.resolve);
-      const startedTracing = promise.defer();
+      yield this._startDebuggingTab();
+
       if (Prefs.tracerEnabled && traceActor) {
-        this._startTracingTab(traceActor, startedTracing.resolve);
-      } else {
-        startedTracing.resolve();
+        yield this._startTracingTab(traceActor);
       }
-
-      return promise.all([startedDebugging.promise, startedTracing.promise]);
     }
-
-    return startedDebugging.promise;
-  },
+  }),
 
   /**
    * Disconnects the debugger client and removes event handlers as necessary.
    */
   disconnect: function() {
     // Return early if the client didn't even have a chance to instantiate.
     if (!this.client) {
       return;
     }
 
-    this._connection = null;
+    this._connected = false;
     this.client = null;
     this.activeThread = null;
   },
 
   /**
    * Called for each location change in the debugged tab.
    *
    * @param string aType
@@ -281,108 +281,120 @@ let DebuggerController = {
     if (aResponse.error == "wrongOrder") {
       DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
     }
   },
 
   /**
    * Sets up a debugging session.
    *
-   * @param function aCallback
-   *        A function to invoke once the client attaches to the active thread.
+   * @return object
+   *         A promise resolved once the client attaches to the active thread.
    */
-  _startDebuggingTab: function(aCallback) {
-    this._target.activeTab.attachThread({
+  _startDebuggingTab: function() {
+    let deferred = promise.defer();
+    let threadOptions = {
       useSourceMaps: Prefs.sourceMapsEnabled
-    }, (aResponse, aThreadClient) => {
+    };
+
+    this._target.activeTab.attachThread(threadOptions, (aResponse, aThreadClient) => {
       if (!aThreadClient) {
-        Cu.reportError("Couldn't attach to thread: " + aResponse.error);
+        deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
         return;
       }
       this.activeThread = aThreadClient;
-
       this.ThreadState.connect();
       this.StackFrames.connect();
       this.SourceScripts.connect();
+
       if (aThreadClient.paused) {
         aThreadClient.resume(this._ensureResumptionOrder);
       }
 
-      if (aCallback) {
-        aCallback();
-      }
+      deferred.resolve();
     });
+
+    return deferred.promise;
   },
 
   /**
    * Sets up an addon debugging session.
    *
    * @param object aAddonActor
    *        The actor for the addon that is being debugged.
-   * @param function aCallback
-   *        A function to invoke once the client attaches to the active thread.
+   * @return object
+   *        A promise resolved once the client attaches to the active thread.
    */
-  _startAddonDebugging: function(aAddonActor, aCallback) {
-    this.client.attachAddon(aAddonActor, (aResponse) => {
-      return this._startChromeDebugging(aResponse.threadActor, aCallback);
+  _startAddonDebugging: function(aAddonActor) {
+    let deferred = promise.defer();
+
+    this.client.attachAddon(aAddonActor, aResponse => {
+      this._startChromeDebugging(aResponse.threadActor).then(deferred.resolve);
     });
+
+    return deferred.promise;
   },
 
   /**
    * Sets up a chrome debugging session.
    *
    * @param object aChromeDebugger
    *        The remote protocol grip of the chrome debugger.
-   * @param function aCallback
-   *        A function to invoke once the client attaches to the active thread.
+   * @return object
+   *         A promise resolved once the client attaches to the active thread.
    */
-  _startChromeDebugging: function(aChromeDebugger, aCallback) {
+  _startChromeDebugging: function(aChromeDebugger) {
+    let deferred = promise.defer();
+    let threadOptions = {
+      useSourceMaps: Prefs.sourceMapsEnabled
+    };
+
     this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
       if (!aThreadClient) {
-        Cu.reportError("Couldn't attach to thread: " + aResponse.error);
+        deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
         return;
       }
       this.activeThread = aThreadClient;
-
       this.ThreadState.connect();
       this.StackFrames.connect();
       this.SourceScripts.connect();
+
       if (aThreadClient.paused) {
         aThreadClient.resume(this._ensureResumptionOrder);
       }
 
-      if (aCallback) {
-        aCallback();
-      }
-    }, { useSourceMaps: Prefs.sourceMapsEnabled });
+      deferred.resolve();
+    }, threadOptions);
+
+    return deferred.promise;
   },
 
   /**
    * Sets up an execution tracing session.
    *
    * @param object aTraceActor
    *        The remote protocol grip of the trace actor.
-   * @param function aCallback
-   *        A function to invoke once the client attaches to the tracer.
+   * @return object
+   *         A promise resolved once the client attaches to the tracer.
    */
-  _startTracingTab: function(aTraceActor, aCallback) {
+  _startTracingTab: function(aTraceActor) {
+    let deferred = promise.defer();
+
     this.client.attachTracer(aTraceActor, (response, traceClient) => {
       if (!traceClient) {
-        DevToolsUtils.reportException("DebuggerController._startTracingTab",
-                                      new Error("Failed to attach to tracing actor."));
+        deferred.reject(new Error("Failed to attach to tracing actor."));
         return;
       }
-
       this.traceClient = traceClient;
       this.Tracer.connect();
 
-      if (aCallback) {
-        aCallback();
-      }
+      deferred.resolve();
     });
+
+    return deferred.promise;
   },
 
   /**
    * Detach and reattach to the thread actor with useSourceMaps true, blow
    * away old sources and get them again.
    */
   reconfigureThread: function(aUseSourceMaps) {
     this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => {
@@ -400,19 +412,19 @@ let DebuggerController = {
       // Update the stack frame list.
       if (this.activeThread.paused) {
         this.activeThread._clearFrames();
         this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
       }
     });
   },
 
-  _startup: null,
-  _shutdown: null,
-  _connection: null,
+  _startup: false,
+  _shutdown: false,
+  _connected: false,
   client: null,
   activeThread: null
 };
 
 /**
  * ThreadState keeps the UI up to date with the state of the
  * thread (paused/attached/etc.).
  */
@@ -594,118 +606,61 @@ StackFrames.prototype = {
     if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
       this._currentWatchExpressions = this._syncedWatchExpressions;
     }
   },
 
   /**
    * Handler for the thread client's framesadded notification.
    */
-  _onFrames: function() {
+  _onFrames: Task.async(function*() {
     // Ignore useless notifications.
     if (!this.activeThread || !this.activeThread.cachedFrames.length) {
       return;
     }
-
-    let waitForNextPause = false;
-    let breakLocation = this._currentBreakpointLocation;
-    let watchExpressions = this._currentWatchExpressions;
-    let client = DebuggerController.activeThread.client;
-
-    // We moved conditional breakpoint handling to the server, but
-    // need to support it in the client for a while until most of the
-    // server code in production is updated with it. bug 990137 is
-    // filed to mark this code to be removed.
-    if (!client.mainRoot.traits.conditionalBreakpoints) {
-      // Conditional breakpoints are { breakpoint, expression } tuples. The
-      // boolean evaluation of the expression decides if the active thread
-      // automatically resumes execution or not.
-      if (breakLocation) {
-        // Make sure a breakpoint actually exists at the specified url and line.
-        let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
-        if (breakpointPromise) {
-          breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
-            // Evaluating the current breakpoint's conditional expression will
-            // cause the stack frames to be cleared and active thread to pause,
-            // sending a 'clientEvaluated' packed and adding the frames again.
-            this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
-            waitForNextPause = true;
-          }});
-        }
-      }
-      // We'll get our evaluation of the current breakpoint's conditional
-      // expression the next time the thread client pauses...
-      if (waitForNextPause) {
-        return;
-      }
-      if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
-        this._currentFrameDescription = FRAME_TYPE.NORMAL;
-        // If the breakpoint's conditional expression evaluation is falsy,
-        // automatically resume execution.
-        if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
-          this.activeThread.resume(DebuggerController._ensureResumptionOrder);
-          return;
-        }
-      }
+    if (this._currentFrameDescription != FRAME_TYPE.NORMAL &&
+        this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
+      return;
     }
 
-    // Watch expressions are evaluated in the context of the topmost frame,
-    // and the results are displayed in the variables view.
+    // TODO: remove all of this deprecated code: Bug 990137.
+    yield this._handleConditionalBreakpoint();
+
     // TODO: handle all of this server-side: Bug 832470, comment 14.
-    if (watchExpressions) {
-      // Evaluation causes the stack frames to be cleared and active thread to
-      // pause, sending a 'clientEvaluated' packet and adding the frames again.
-      this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
-      waitForNextPause = true;
-    }
-    // We'll get our evaluation of the current watch expressions the next time
-    // the thread client pauses...
-    if (waitForNextPause) {
-      return;
-    }
-    if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
-      this._currentFrameDescription = FRAME_TYPE.NORMAL;
-      // If an error was thrown during the evaluation of the watch expressions,
-      // then at least one expression evaluation could not be performed. So
-      // remove the most recent watch expression and try again.
-      if (this._currentEvaluation.throw) {
-        DebuggerView.WatchExpressions.removeAt(0);
-        DebuggerController.StackFrames.syncWatchExpressions();
-        return;
-      }
-    }
+    yield this._handleWatchExpressions();
 
     // Make sure the debugger view panes are visible, then refill the frames.
     DebuggerView.showInstrumentsPane();
     this._refillFrames();
 
     // No additional processing is necessary for this stack frame.
     if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
       this._currentFrameDescription = FRAME_TYPE.NORMAL;
     }
-  },
+  }),
 
   /**
    * Fill the StackFrames view with the frames we have in the cache, compressing
    * frames which have black boxed sources into single frames.
    */
   _refillFrames: function() {
     // Make sure all the previous stackframes are removed before re-adding them.
     DebuggerView.StackFrames.empty();
-
     for (let frame of this.activeThread.cachedFrames) {
       let { depth, where: { url, line }, source } = frame;
       let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
       let location = NetworkHelper.convertToUnicode(unescape(url));
       let title = StackFrameUtils.getFrameTitle(frame);
       DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
     }
 
     DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
     DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
+
+    window.emit(EVENTS.AFTER_FRAMES_REFILLED);
   },
 
   /**
    * Handler for the thread client's framescleared notification.
    */
   _onFramesCleared: function() {
     switch (this._currentFrameDescription) {
       case FRAME_TYPE.NORMAL:
@@ -738,24 +693,26 @@ StackFrames.prototype = {
       this._refillFrames();
     }
   },
 
   /**
    * Handler for the debugger's prettyprintchange notification.
    */
   _onPrettyPrintChange: function() {
+    if (this.activeThread.state != "paused") {
+      return;
+    }
     // Makes sure the selected source remains selected
     // after the fillFrames is called.
     const source = DebuggerView.Sources.selectedValue;
-    if (this.activeThread.state == "paused") {
-      this.activeThread.fillFrames(
-         CALL_STACK_PAGE_SIZE,
-         () => DebuggerView.Sources.selectedValue = source);
-    }
+
+    this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => {
+      DebuggerView.Sources.selectedValue = source;
+    });
   },
 
   /**
    * Called soon after the thread client's framescleared notification.
    */
   _afterFramesCleared: function() {
     // Ignore useless notifications.
     if (this.activeThread.cachedFrames.length) {
@@ -934,16 +891,97 @@ StackFrames.prototype = {
     // Add "this".
     if (aFrame.this) {
       let thisRef = aScope.addItem("this", { value: aFrame.this });
       DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
     }
   },
 
   /**
+   * Handles conditional breakpoints when the debugger pauses and the
+   * stackframes are received.
+   *
+   * We moved conditional breakpoint handling to the server, but
+   * need to support it in the client for a while until most of the
+   * server code in production is updated with it.
+   * TODO: remove all of this deprecated code: Bug 990137.
+   *
+   * @return object
+   *         A promise that is resolved after a potential breakpoint's
+   *         conditional expression is evaluated. If there's no breakpoint
+   *         where the debugger is paused, the promise is resolved immediately.
+   */
+  _handleConditionalBreakpoint: Task.async(function*() {
+    if (gClient.mainRoot.traits.conditionalBreakpoints) {
+      return;
+    }
+    let breakLocation = this._currentBreakpointLocation;
+    if (!breakLocation) {
+      return;
+    }
+    let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
+    if (!breakpointPromise) {
+      return;
+    }
+    let breakpointClient = yield breakpointPromise;
+    let conditionalExpression = breakpointClient.conditionalExpression;
+    if (!conditionalExpression) {
+      return;
+    }
+
+    // Evaluating the current breakpoint's conditional expression will
+    // cause the stack frames to be cleared and active thread to pause,
+    // sending a 'clientEvaluated' packed and adding the frames again.
+    let evaluationOptions = { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL };
+    yield this.evaluate(conditionalExpression, evaluationOptions);
+    this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+    // If the breakpoint's conditional expression evaluation is falsy,
+    // automatically resume execution.
+    if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
+      this.activeThread.resume(DebuggerController._ensureResumptionOrder);
+    }
+  }),
+
+  /**
+   * Handles watch expressions when the debugger pauses and the stackframes
+   * are received.
+   *
+   * @return object
+   *         A promise that is resolved after the potential watch expressions
+   *         are evaluated. If there are no watch expressions where the debugger
+   *         is paused, the promise is resolved immediately.
+   */
+  _handleWatchExpressions: Task.async(function*() {
+    // Ignore useless notifications.
+    if (!this.activeThread || !this.activeThread.cachedFrames.length) {
+      return;
+    }
+
+    let watchExpressions = this._currentWatchExpressions;
+    if (!watchExpressions) {
+      return;
+    }
+
+    // Evaluation causes the stack frames to be cleared and active thread to
+    // pause, sending a 'clientEvaluated' packet and adding the frames again.
+    let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL };
+    yield this.evaluate(watchExpressions, evaluationOptions);
+    this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+    // If an error was thrown during the evaluation of the watch expressions,
+    // then at least one expression evaluation could not be performed. So
+    // remove the most recent watch expression and try again.
+    if (this._currentEvaluation.throw) {
+      DebuggerView.WatchExpressions.removeAt(0);
+      yield DebuggerController.StackFrames.syncWatchExpressions();
+    }
+  }),
+
+  /**
    * Adds the watch expressions evaluation results to a scope in the view.
    *
    * @param Scope aScope
    *        The scope where the watch expressions will be placed into.
    * @param object aExp
    *        The grip of the evaluation results.
    */
   _fetchWatchExpressions: function(aScope, aExp) {
@@ -993,40 +1031,39 @@ StackFrames.prototype = {
       try {
         Parser.reflectionAPI.parse(aString);
         return aString; // Watch expression can be executed safely.
       } catch (e) {
         return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
       }
     });
 
-    if (sanitizedExpressions.length) {
-      this._syncedWatchExpressions =
-        this._currentWatchExpressions =
-          "[" +
-            sanitizedExpressions.map(aString =>
-              "eval(\"" +
-                "try {" +
-                  // Make sure all quotes are escaped in the expression's syntax,
-                  // and add a newline after the statement to avoid comments
-                  // breaking the code integrity inside the eval block.
-                  aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
-                "} catch (e) {" +
-                  "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
-                "}" +
-              "\")"
-            ).join(",") +
-          "]";
+    if (!sanitizedExpressions.length) {
+      this._currentWatchExpressions = null;
+      this._syncedWatchExpressions = null;
     } else {
       this._syncedWatchExpressions =
-        this._currentWatchExpressions = null;
+      this._currentWatchExpressions = "[" +
+        sanitizedExpressions.map(aString =>
+          "eval(\"" +
+            "try {" +
+              // Make sure all quotes are escaped in the expression's syntax,
+              // and add a newline after the statement to avoid comments
+              // breaking the code integrity inside the eval block.
+              aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
+            "} catch (e) {" +
+              "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
+            "}" +
+          "\")"
+        ).join(",") +
+      "]";
     }
 
     this.currentFrameDepth = -1;
-    this._onFrames();
+    return this._onFrames();
   }
 };
 
 /**
  * Keeps the source script list up-to-date, using the thread client's
  * source script cache.
  */
 function SourceScripts() {
@@ -1317,23 +1354,22 @@ SourceScripts.prototype = {
     this._cache.set(aSource.url, deferred.promise);
 
     // If the source text takes a long time to fetch, invoke a callback.
     if (aOnTimeout) {
       var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
     }
 
     // Get the source text from the active thread.
-    this.activeThread.source(aSource)
-        .source(({ error, message, source: text, contentType }) => {
+    this.activeThread.source(aSource).source(({ error, source: text, contentType }) => {
       if (aOnTimeout) {
         window.clearTimeout(fetchTimeout);
       }
       if (error) {
-        deferred.reject([aSource, message || error]);
+        deferred.reject([aSource, error]);
       } else {
         deferred.resolve([aSource, text, contentType]);
       }
     });
 
     return deferred.promise;
   },
 
@@ -1436,31 +1472,35 @@ Tracer.prototype = {
     this._stack = null;
     this.client.removeListener("traces", this.onTraces);
   },
 
   /**
    * Instructs the tracer actor to start tracing.
    */
   startTracing: function(aCallback = () => {}) {
-    DebuggerView.Tracer.selectTab();
     if (this.tracing) {
       return;
     }
-    this._trace = "dbg.trace" + Math.random();
-    this.traceClient.startTrace([
+
+    DebuggerView.Tracer.selectTab();
+
+    let id = this._trace = "dbg.trace" + Math.random();
+    let fields = [
       "name",
       "location",
       "parameterNames",
       "depth",
       "arguments",
       "return",
       "throw",
       "yield"
-    ], this._trace, (aResponse) => {
+    ];
+
+    this.traceClient.startTrace(fields, id, aResponse => {
       const { error } = aResponse;
       if (error) {
         DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
         this._trace = null;
       }
 
       aCallback(aResponse);
     });
@@ -1482,21 +1522,21 @@ Tracer.prototype = {
       this._trace = null;
       aCallback(aResponse);
     });
   },
 
   onTraces: function (aEvent, { traces }) {
     const tracesLength = traces.length;
     let tracesToShow;
+
     if (tracesLength > TracerView.MAX_TRACES) {
-      tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
-                                  tracesLength);
+      tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, tracesLength);
+      this._stack.splice(0, this._stack.length);
       DebuggerView.Tracer.empty();
-      this._stack.splice(0, this._stack.length);
     } else {
       tracesToShow = traces;
     }
 
     for (let t of tracesToShow) {
       if (t.type == "enteredFrame") {
         this._onCall(t);
       } else {
@@ -1609,17 +1649,16 @@ Tracer.prototype = {
     this.object = aObject;
   }
 };
 
 /**
  * Handles breaking on event listeners in the currently debugged target.
  */
 function EventListeners() {
-  this._onEventListeners = this._onEventListeners.bind(this);
 }
 
 EventListeners.prototype = {
   /**
    * A list of event names on which the debuggee will automatically pause
    * when invoked.
    */
   activeEventNames: [],
@@ -1637,75 +1676,81 @@ EventListeners.prototype = {
       gThreadClient.pauseOnDOMEvents(this.activeEventNames);
 
       // Notify that event breakpoints were added/removed on the server.
       window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
     });
   },
 
   /**
-   * Fetches the currently attached event listeners from the debugee.
+   * Schedules fetching the currently attached event listeners from the debugee.
    */
   scheduleEventListenersFetch: function() {
-    let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
-      if (aResponse.error) {
-        let msg = "Error getting event listeners: " + aResponse.message;
-        DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
-        return;
-      }
-
-      let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
-        const deferred = promise.defer();
-
-        gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
-          if (aResponse.error) {
-            const msg = "Error getting function definition site: " + aResponse.message;
-            DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
-          } else {
-            aListener.function.url = aResponse.url;
-          }
-
-          deferred.resolve(aListener);
-        });
-
-        return deferred.promise;
-      });
-
-      promise.all(outstandingListenersDefinitionSite).then(aListeners => {
-        this._onEventListeners(aListeners);
-
-        // Notify that event listeners were fetched and shown in the view,
-        // and callback to resume the active thread if necessary.
-        window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
-        aCallback && aCallback();
-      });
-    });
-
     // Make sure we're not sending a batch of closely repeated requests.
     // This can easily happen whenever new sources are fetched.
     setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
       if (gThreadClient.state != "paused") {
-        gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
+        gThreadClient.interrupt(() => this._getListeners(() => gThreadClient.resume()));
       } else {
-        getListeners();
+        this._getListeners();
       }
     });
   },
 
   /**
-   * Callback for a debugger's successful active thread eventListeners() call.
+   * Fetches the currently attached event listeners from the debugee.
+   * The thread client state is assumed to be "paused".
+   *
+   * @param function aCallback
+   *        Invoked once the event listeners are fetched and displayed.
    */
-  _onEventListeners: function(aListeners) {
-    // Add all the listeners in the debugger view event linsteners container.
-    for (let listener of aListeners) {
-      DebuggerView.EventListeners.addListener(listener, { staged: true });
-    }
+  _getListeners: function(aCallback) {
+    gThreadClient.eventListeners(Task.async(function*(aResponse) {
+      if (aResponse.error) {
+        throw "Error getting event listeners: " + aResponse.message;
+      }
+
+      // Add all the listeners in the debugger view event linsteners container.
+      for (let listener of aResponse.listeners) {
+        let definitionSite = yield this._getDefinitionSite(listener.function);
+        listener.function.url = definitionSite;
+        DebuggerView.EventListeners.addListener(listener, { staged: true });
+      }
+
+      // Flushes all the prepared events into the event listeners container.
+      DebuggerView.EventListeners.commit();
 
-    // Flushes all the prepared events into the event listeners container.
-    DebuggerView.EventListeners.commit();
+      // Notify that event listeners were fetched and shown in the view,
+      // and callback to resume the active thread if necessary.
+      window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
+      aCallback && aCallback();
+    }.bind(this)));
+  },
+
+  /**
+   * Gets a function's source-mapped definiton site.
+   *
+   * @param object aFunction
+   *        The grip of the function to get the definition site for.
+   * @return object
+   *         A promise that is resolved with the function's owner source url,
+   *         or rejected if an error occured.
+   */
+  _getDefinitionSite: function(aFunction) {
+    let deferred = promise.defer();
+
+    gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => {
+      if (aResponse.error) {
+        deferred.reject("Error getting function definition site: " + aResponse.message);
+      } else {
+        deferred.resolve(aResponse.url);
+      }
+    });
+
+    return deferred.promise;
   }
 };
 
 /**
  * Handles all the breakpoints in the current debugger.
  */
 function Breakpoints() {
   this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
@@ -1751,94 +1796,90 @@ Breakpoints.prototype = {
   },
 
   /**
    * Event handler for new breakpoints that come from the editor.
    *
    * @param number aLine
    *        Line number where breakpoint was set.
    */
-  _onEditorBreakpointAdd: function(_, aLine) {
+  _onEditorBreakpointAdd: Task.async(function*(_, aLine) {
     let url = DebuggerView.Sources.selectedValue;
     let location = { url: url, line: aLine + 1 };
+    let breakpointClient = yield this.addBreakpoint(location, { noEditorUpdate: true });
 
     // Initialize the breakpoint, but don't update the editor, since this
     // callback is invoked because a breakpoint was added in the editor itself.
     this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => {
       // If the breakpoint client has a "requestedLocation" attached, then
       // the original requested placement for the breakpoint wasn't accepted.
       // In this case, we need to update the editor with the new location.
-      if (aBreakpointClient.requestedLocation) {
+      if (breakpointClient.requestedLocation) {
         DebuggerView.editor.moveBreakpoint(
-          aBreakpointClient.requestedLocation.line - 1,
-          aBreakpointClient.location.line - 1
+          breakpointClient.requestedLocation.line - 1,
+          breakpointClient.location.line - 1
         );
       }
       // Notify that we've shown a breakpoint in the source editor.
-      window.emit(EVENTS.BREAKPOINT_SHOWN);
+      window.emit(EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
     });
-  },
+  }),
 
   /**
    * Event handler for breakpoints that are removed from the editor.
    *
    * @param number aLine
    *        Line number where breakpoint was removed.
    */
-  _onEditorBreakpointRemove: function(_, aLine) {
+  _onEditorBreakpointRemove: Task.async(function*(_, aLine) {
     let url = DebuggerView.Sources.selectedValue;
     let location = { url: url, line: aLine + 1 };
+    yield this.removeBreakpoint(location, { noEditorUpdate: true });
 
-    // Destroy the breakpoint, but don't update the editor, since this callback
-    // is invoked because a breakpoint was removed from the editor itself.
-    this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
-      // Notify that we've hidden a breakpoint in the source editor.
-      window.emit(EVENTS.BREAKPOINT_HIDDEN);
-    });
-  },
+    // Notify that we've hidden a breakpoint in the source editor.
+    window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_EDITOR);
+  }),
 
   /**
    * Update the breakpoints in the editor view. This function takes the list of
    * breakpoints in the debugger and adds them back into the editor view.
    * This is invoked when the selected script is changed, or when new sources
    * are received via the _onNewSource and _onSourcesAdded event listeners.
    */
-  updateEditorBreakpoints: function() {
+  updateEditorBreakpoints: Task.async(function*() {
     for (let breakpointPromise of this._addedOrDisabled) {
-      breakpointPromise.then(aBreakpointClient => {
-        let currentSourceUrl = DebuggerView.Sources.selectedValue;
-        let breakpointUrl = aBreakpointClient.location.url;
+      let breakpointClient = yield breakpointPromise;
+      let currentSourceUrl = DebuggerView.Sources.selectedValue;
+      let breakpointUrl = breakpointClient.location.url;
 
-        // Update the view only if the breakpoint is in the currently shown source.
-        if (currentSourceUrl == breakpointUrl) {
-          this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
-        }
-      });
+      // Update the view only if the breakpoint is in the currently shown source.
+      if (currentSourceUrl == breakpointUrl) {
+        this._showBreakpoint(breakpointClient, { noPaneUpdate: true });
+      }
     }
-  },
+  }),
 
   /**
    * Update the breakpoints in the pane view. This function takes the list of
    * breakpoints in the debugger and adds them back into the breakpoints pane.
    * This is invoked when new sources are received via the _onNewSource and
    * _onSourcesAdded event listeners.
    */
-  updatePaneBreakpoints: function() {
+  updatePaneBreakpoints: Task.async(function*() {
     for (let breakpointPromise of this._addedOrDisabled) {
-      breakpointPromise.then(aBreakpointClient => {
-        let container = DebuggerView.Sources;
-        let breakpointUrl = aBreakpointClient.location.url;
+      let breakpointClient = yield breakpointPromise;
+      let container = DebuggerView.Sources;
+      let breakpointUrl = breakpointClient.location.url;
 
-        // Update the view only if the breakpoint exists in a known source.
-        if (container.containsValue(breakpointUrl)) {
-          this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
-        }
-      });
+      // Update the view only if the breakpoint exists in a known source.
+      if (container.containsValue(breakpointUrl)) {
+        this._showBreakpoint(breakpointClient, { noEditorUpdate: true });
+      }
     }
-  },
+  }),
 
   /**
    * Add a breakpoint.
    *
    * @param object aLocation
    *        The location where you want the breakpoint.
    *        This object must have two properties:
    *          - url: the breakpoint's source location.
@@ -2043,32 +2084,29 @@ Breakpoints.prototype = {
    *
    * @param object aLocation
    *        @see DebuggerController.Breakpoints.addBreakpoint
    * @param string aClients
    *        The condition to set on the breakpoint
    * @return object
    *         A promise that will be resolved with the breakpoint client
    */
-  updateCondition: function(aLocation, aCondition) {
+  updateCondition: Task.async(function*(aLocation, aCondition) {
     let addedPromise = this._getAdded(aLocation);
     if (!addedPromise) {
-      return promise.reject(new Error('breakpoint does not exist ' +
-                                      'in specified location'));
+      throw new Error("Breakpoint does not exist at the specified location");
     }
-
-    var promise = addedPromise.then(aBreakpointClient => {
-      return aBreakpointClient.setCondition(gThreadClient, aCondition);
-    });
+    let breakpointClient = yield addedPromise;
+    let promise = breakpointClient.setCondition(gThreadClient, aCondition);
 
     // `setCondition` returns a new breakpoint that has the condition,
     // so we need to update the store
     this._added.set(this.getIdentifier(aLocation), promise);
     return promise;
-  },
+  }),
 
   /**
    * Update the editor and breakpoints pane to show a specified breakpoint.
    *
    * @param object aBreakpointData
    *        Information about the breakpoint to be shown.
    *        This object must have the following properties:
    *          - location: the breakpoint's source location and line number
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1,19 +1,15 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 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/. */
-
 "use strict";
 
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-                                  "resource://gre/modules/Task.jsm");
-
 // Used to detect minification for automatic pretty printing
 const SAMPLE_SIZE = 50; // no of lines
 const INDENT_COUNT_THRESHOLD = 5; // percentage
 const CHARACTER_LIMIT = 250; // line character limit
 
 // Maps known URLs to friendly source group names
 const KNOWN_SOURCE_GROUPS = {
   "Add-on SDK": "resource://gre/modules/commonjs/",
@@ -36,40 +32,29 @@ function SourcesView() {
   this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
   this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
   this._onBreakpointClick = this._onBreakpointClick.bind(this);
   this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
   this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
   this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
   this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
   this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
-
-  this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
 }
 
 SourcesView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function() {
     dumpn("Initializing the SourcesView");
 
     this.widget = new SideMenuWidget(document.getElementById("sources"), {
       showArrows: true
     });
 
-    // Sort known source groups towards the end of the list
-    this.widget.groupSortPredicate = function(a, b) {
-      if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
-        return a.localeCompare(b);
-      }
-
-      return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
-    };
-
     this.emptyText = L10N.getStr("noSourcesText");
     this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
 
     this._commandset = document.getElementById("debuggerCommands");
     this._popupset = document.getElementById("debuggerPopupset");
     this._cmPopup = document.getElementById("sourceEditorContextMenu");
     this._cbPanel = document.getElementById("conditional-breakpoint-panel");
     this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
@@ -93,16 +78,24 @@ SourcesView.prototype = Heritage.extend(
 
     this.autoFocusOnSelection = false;
 
     // Sort the contents by the displayed label.
     this.sortContents((aFirst, aSecond) => {
       return +(aFirst.attachment.label.toLowerCase() >
                aSecond.attachment.label.toLowerCase());
     });
+
+    // Sort known source groups towards the end of the list
+    this.widget.groupSortPredicate = function(a, b) {
+      if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
+        return a.localeCompare(b);
+      }
+      return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
+    };
   },
 
   /**
    * Destruction function, called when the debugger is closed.
    */
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
@@ -211,16 +204,18 @@ SourcesView.prototype = Heritage.extend(
       // menupopup and commandset are also destroyed.
       finalize: this._onBreakpointRemoved
     });
 
     // Highlight the newly appended breakpoint child item if necessary.
     if (aOptions.openPopup || !aOptions.noEditorUpdate) {
       this.highlightBreakpoint(location, aOptions);
     }
+
+    window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE);
   },
 
   /**
    * Removes a breakpoint from this sources container.
    * It does not also remove the breakpoint from the controller. Be careful.
    *
    * @param object aLocation
    *        @see DebuggerController.Breakpoints.addBreakpoint
@@ -234,16 +229,18 @@ SourcesView.prototype = Heritage.extend(
     }
     let breakpointItem = this.getBreakpoint(aLocation);
     if (!breakpointItem) {
       return;
     }
 
     // Clear the breakpoint view.
     sourceItem.remove(breakpointItem);
+
+    window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE);
   },
 
   /**
    * Returns the breakpoint at the specified source url and line.
    *
    * @param object aLocation
    *        @see DebuggerController.Breakpoints.addBreakpoint
    * @return object
@@ -424,18 +421,18 @@ SourcesView.prototype = Heritage.extend(
     let location = { url: url, line: line };
     this.highlightBreakpoint(location, { noEditorUpdate: true });
   },
 
   /**
    * Unhighlights the current breakpoint in this sources container.
    */
   unhighlightBreakpoint: function() {
+    this._hideConditionalPopup();
     this._unselectBreakpoint();
-    this._hideConditionalPopup();
   },
 
   /**
    * Update the checked/unchecked and enabled/disabled states of the buttons in
    * the sources toolbar based on the currently selected source's state.
    */
   updateToolbarButtonsState: function() {
     const { source } = this.selectedItem.attachment;
@@ -454,17 +451,17 @@ SourcesView.prototype = Heritage.extend(
     } else {
       this._prettyPrintButton.removeAttribute("checked");
     }
   },
 
   /**
    * Toggle the pretty printing of the selected source.
    */
-  togglePrettyPrint: function() {
+  togglePrettyPrint: Task.async(function*() {
     if (this._prettyPrintButton.hasAttribute("disabled")) {
       return;
     }
 
     const resetEditor = ([{ url }]) => {
       // Only set the text when the source is still selected.
       if (url == this.selectedValue) {
         DebuggerView.setEditorLocation(url, 0, { force: true });
@@ -481,48 +478,56 @@ SourcesView.prototype = Heritage.extend(
     const shouldPrettyPrint = !sourceClient.isPrettyPrinted;
 
     if (shouldPrettyPrint) {
       this._prettyPrintButton.setAttribute("checked", true);
     } else {
       this._prettyPrintButton.removeAttribute("checked");
     }
 
-    DebuggerController.SourceScripts.togglePrettyPrint(source)
-      .then(resetEditor, printError)
-      .then(DebuggerView.showEditor)
-      .then(this.updateToolbarButtonsState);
-  },
+    try {
+      let resolution = yield DebuggerController.SourceScripts.togglePrettyPrint(source);
+      resetEditor(resolution);
+    } catch (rejection) {
+      printError(rejection);
+    }
+
+    DebuggerView.showEditor();
+    this.updateToolbarButtonsState();
+  }),
 
   /**
    * Toggle the black boxed state of the selected source.
    */
-  toggleBlackBoxing: function() {
+  toggleBlackBoxing: Task.async(function*() {
     const { source } = this.selectedItem.attachment;
     const sourceClient = gThreadClient.source(source);
     const shouldBlackBox = !sourceClient.isBlackBoxed;
 
     // Be optimistic that the (un-)black boxing will succeed, so enable/disable
-    // the pretty print button and check/uncheck the black box button
-    // immediately. Then, once we actually get the results from the server, make
-    // sure that it is in the correct state again by calling
-    // `updateToolbarButtonsState`.
+    // the pretty print button and check/uncheck the black box button immediately.
+    // Then, once we actually get the results from the server, make sure that
+    // it is in the correct state again by calling `updateToolbarButtonsState`.
 
     if (shouldBlackBox) {
       this._prettyPrintButton.setAttribute("disabled", true);
       this._blackBoxButton.setAttribute("checked", true);
     } else {
       this._prettyPrintButton.removeAttribute("disabled");
       this._blackBoxButton.removeAttribute("checked");
     }
 
-    DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
-      .then(this.updateToolbarButtonsState,
-            this.updateToolbarButtonsState);
-  },
+    try {
+      yield DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox);
+    } catch (e) {
+      // Continue execution in this task even if blackboxing failed.
+    }
+
+    this.updateToolbarButtonsState();
+  }),
 
   /**
    * Toggles all breakpoints enabled/disabled.
    */
   toggleBreakpoints: function() {
     let breakpoints = this.getAllBreakpoints();
     let hasBreakpoints = breakpoints.length > 0;
     let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled);
@@ -801,52 +806,56 @@ SourcesView.prototype = Heritage.extend(
     } else {
       this.unhighlightBreakpoint();
     }
   },
 
   /**
    * The select listener for the sources container.
    */
-  _onSourceSelect: function({ detail: sourceItem }) {
+  _onSourceSelect: Task.async(function*({ detail: sourceItem }) {
     if (!sourceItem) {
       return;
     }
     const { source } = sourceItem.attachment;
     const sourceClient = gThreadClient.source(source);
 
     // The container is not empty and an actual item was selected.
     DebuggerView.setEditorLocation(sourceItem.value);
 
+    // Attempt to automatically pretty print minified source code.
     if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) {
-      DebuggerController.SourceScripts.getText(source).then(([, aText]) => {
-        if (SourceUtils.isMinified(sourceClient, aText)) {
-          this.togglePrettyPrint();
-        }
-      }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e));
+      let isMinified = yield SourceUtils.isMinified(sourceClient);
+      if (isMinified) {
+        this.togglePrettyPrint();
+      }
     }
 
     // Set window title. No need to split the url by " -> " here, because it was
     // already sanitized when the source was added.
     document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value);
 
     DebuggerView.maybeShowBlackBoxMessage();
     this.updateToolbarButtonsState();
-  },
+  }),
 
   /**
    * The click listener for the "stop black boxing" button.
    */
-  _onStopBlackBoxing: function() {
+  _onStopBlackBoxing: Task.async(function*() {
     const { source } = this.selectedItem.attachment;
 
-    DebuggerController.SourceScripts.setBlackBoxing(source, false)
-      .then(this.updateToolbarButtonsState,
-            this.updateToolbarButtonsState);
-  },
+    try {
+      yield DebuggerController.SourceScripts.setBlackBoxing(source, false);
+    } catch (e) {
+      // Continue execution in this task even if blackboxing failed.
+    }
+
+    this.updateToolbarButtonsState();
+  }),
 
   /**
    * The click listener for a breakpoint container.
    */
   _onBreakpointClick: function(e) {
     let sourceItem = this.getItemForElement(e.target);
     let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
     let attachment = breakpointItem.attachment;
@@ -908,28 +917,27 @@ SourcesView.prototype = Heritage.extend(
     this._cbTextbox.select();
   },
 
   /**
    * The popup hiding listener for the breakpoints conditional expression panel.
    */
   _onConditionalPopupHiding: Task.async(function*() {
     this._conditionalPopupVisible = false; // Used in tests.
+
     let breakpointItem = this._selectedBreakpointItem;
     let attachment = breakpointItem.attachment;
 
     // Check if this is an enabled conditional breakpoint, and if so,
     // save the current conditional epression.
     let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
     if (breakpointPromise) {
-      let breakpointClient = yield breakpointPromise;
-      yield DebuggerController.Breakpoints.updateCondition(
-        breakpointClient.location,
-        this._cbTextbox.value
-      );
+      let { location } = yield breakpointPromise;
+      let condition = this._cbTextbox.value;
+      yield DebuggerController.Breakpoints.updateCondition(location, condition);
     }
 
     window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
   }),
 
   /**
    * The keypress listener for the breakpoints conditional expression textbox.
    */
@@ -1119,17 +1127,18 @@ function TracerView() {
   this._onStartTracing =
     DevToolsUtils.makeInfallible(this._onStartTracing.bind(this));
   this._onClear =
     DevToolsUtils.makeInfallible(this._onClear.bind(this));
   this._onSelect =
     DevToolsUtils.makeInfallible(this._onSelect.bind(this));
   this._onMouseOver =
     DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
-  this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
+  this._onSearch =
+    DevToolsUtils.makeInfallible(this._onSearch.bind(this));
 }
 
 TracerView.MAX_TRACES = 200;
 
 TracerView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
@@ -1252,16 +1261,17 @@ TracerView.prototype = Heritage.extend(W
    *        The name of the variable.
    * @param Object aParent
    *        The parent scope.
    * @param Object aValue
    *        The value of the variable.
    */
   _populateVariable: function(aName, aParent, aValue) {
     let item = aParent.addItem(aName, { value: aValue });
+
     if (aValue) {
       let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue);
       DebuggerView.Variables.controller.populate(item, wrappedValue);
       item.expand();
       item.twisty = false;
     }
   },
 
@@ -1507,57 +1517,57 @@ let SourceUtils = {
     return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) ||
            aContentType.contains("javascript");
   },
 
   /**
    * Determines if the source text is minified by using
    * the percentage indented of a subset of lines
    *
-   * @param string aText
-   *        The source text.
-   * @return boolean
-   *        True if source text is minified.
+   * @return object
+   *         A promise that resolves to true if source text is minified.
    */
-  isMinified: function(sourceClient, aText){
+  isMinified: Task.async(function*(sourceClient) {
     if (this._minifiedCache.has(sourceClient)) {
       return this._minifiedCache.get(sourceClient);
     }
 
+    let [, text] = yield DebuggerController.SourceScripts.getText(sourceClient);
     let isMinified;
     let lineEndIndex = 0;
     let lineStartIndex = 0;
     let lines = 0;
     let indentCount = 0;
     let overCharLimit = false;
 
     // Strip comments.
-    aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
+    text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
 
     while (lines++ < SAMPLE_SIZE) {
-      lineEndIndex = aText.indexOf("\n", lineStartIndex);
+      lineEndIndex = text.indexOf("\n", lineStartIndex);
       if (lineEndIndex == -1) {
          break;
       }
-      if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) {
+      if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
         indentCount++;
       }
       // For files with no indents but are not minified.
       if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
         overCharLimit = true;
         break;
       }
       lineStartIndex = lineEndIndex + 1;
     }
-    isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD ||
-                 overCharLimit;
+
+    isMinified =
+      ((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
 
     this._minifiedCache.set(sourceClient, isMinified);
     return isMinified;
-  },
+  }),
 
   /**
    * Clears the labels, groups and minify cache, populated by methods like
    * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
    * This should be done every time the content location changes.
    */
   clearCache: function() {
     this._labelsCache.clear();
@@ -1585,16 +1595,17 @@ let SourceUtils = {
       if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
         sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
       }
     }
 
     if (!sourceLabel) {
       sourceLabel = this.trimUrl(aUrl);
     }
+
     let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
     this._labelsCache.set(aUrl, unicodeLabel);
     return unicodeLabel;
   },
 
   /**
    * Gets as much information as possible about the hostname and directory paths
    * of an url to create a short url group identifier.
@@ -2323,22 +2334,21 @@ WatchExpressionsView.prototype = Heritag
     // Synchronize with the controller's watch expressions store.
     DebuggerController.StackFrames.syncWatchExpressions();
   },
 
   /**
    * The keypress listener for a watch expression's textbox.
    */
   _onKeyPress: function(e) {
-    switch(e.keyCode) {
+    switch (e.keyCode) {
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ESCAPE:
         e.stopPropagation();
         DebuggerView.editor.focus();
-        return;
     }
   }
 });
 
 /**
  * Functions handling the event listeners UI.
  */
 function EventListenersView() {
@@ -2399,16 +2409,17 @@ EventListenersView.prototype = Heritage.
       url = this._inNativeCodeString;
     }
 
     // If an event item for this listener's url and type was already added,
     // avoid polluting the view and simply increase the "targets" count.
     let eventItem = this.getItemForPredicate(aItem =>
       aItem.attachment.url == url &&
       aItem.attachment.type == type);
+
     if (eventItem) {
       let { selectors, view: { targets } } = eventItem.attachment;
       if (selectors.indexOf(selector) == -1) {
         selectors.push(selector);
         targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
       }
       return;
     }
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -115,16 +115,20 @@ ToolbarView.prototype = {
       delayed: true
     });
   },
 
   /**
    * Listener handling the pause/resume button click event.
    */
   _onResumePressed: function() {
+    if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
+      return;
+    }
+
     if (DebuggerController.activeThread.paused) {
       let warn = DebuggerController._ensureResumptionOrder;
       DebuggerController.StackFrames.currentFrameDepth = -1;
       DebuggerController.activeThread.resume(warn);
     } else {
       DebuggerController.activeThread.interrupt();
     }
   },
@@ -139,16 +143,20 @@ ToolbarView.prototype = {
       DebuggerController.activeThread.stepOver(warn);
     }
   },
 
   /**
    * Listener handling the step in button click event.
    */
   _onStepInPressed: function() {
+    if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
+      return;
+    }
+
     if (DebuggerController.activeThread.paused) {
       DebuggerController.StackFrames.currentFrameDepth = -1;
       let warn = DebuggerController._ensureResumptionOrder;
       DebuggerController.activeThread.stepIn(warn);
     }
   },
 
   /**
@@ -310,17 +318,17 @@ OptionsView.prototype = {
       this._showOriginalSourceItem.getAttribute("checked") == "true";
 
     // Don't block the UI while reconfiguring the server.
     window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => {
       // The popup panel needs more time to hide after triggering onpopuphidden.
       window.setTimeout(() => {
         DebuggerController.reconfigureThread(pref);
       }, POPUP_HIDDEN_DELAY);
-    }, false);
+    });
   },
 
   _button: null,
   _pauseOnExceptionsItem: null,
   _showPanesOnStartupItem: null,
   _showVariablesOnlyEnumItem: null,
   _showVariablesFilterBoxItem: null,
   _showOriginalSourceItem: null
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -52,17 +52,17 @@
              oncommand="DebuggerView.Toolbar._onStepInPressed()"/>
     <command id="stepOutCommand"
              oncommand="DebuggerView.Toolbar._onStepOutPressed()"/>
     <command id="fileSearchCommand"
              oncommand="DebuggerView.Filtering._doFileSearch()"/>
     <command id="globalSearchCommand"
              oncommand="DebuggerView.Filtering._doGlobalSearch()"/>
     <command id="functionSearchCommand"
-             oncommand="DepbuggerView.Filtering._doFunctionSearch()"/>
+             oncommand="DebuggerView.Filtering._doFunctionSearch()"/>
     <command id="tokenSearchCommand"
              oncommand="DebuggerView.Filtering._doTokenSearch()"/>
     <command id="lineSearchCommand"
              oncommand="DebuggerView.Filtering._doLineSearch()"/>
     <command id="variableSearchCommand"
              oncommand="DebuggerView.Filtering._doVariableSearch()"/>
     <command id="variablesFocusCommand"
              oncommand="DebuggerView.Filtering._doVariablesFocus()"/>
--- a/browser/devtools/debugger/test/browser_dbg_breakpoints-contextmenu-add.js
+++ b/browser/devtools/debugger/test/browser_dbg_breakpoints-contextmenu-add.js
@@ -54,17 +54,17 @@ function test() {
   function testAddBreakpoint() {
     gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
     gEditor.emit("gutterClick", 6, 2);
 
     return once(gContextMenu, "popupshown").then(() => {
       is(gBreakpointsAdded.size, 0, "no breakpoints added");
 
       let cmd = gContextMenu.querySelector('menuitem[command=addBreakpointCommand]');
-      let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN);
+      let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
       EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger);
       return bpShown;
     }).then(() => {
       is(gBreakpointsAdded.size, 1,
          "1 breakpoint correctly added");
       is(gEditor.getBreakpoints().length, 1,
          "1 breakpoint currently shown in the editor.");
       ok(gBreakpoints._getAdded({ url: gSources.values[1], line: 7 }),
--- a/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints-01.js
@@ -46,17 +46,22 @@ function test() {
         // conditional expression evaluates to undefined. It used to
         // be on line 30, but it can't be the last breakpoint because
         // there is a race condition (the "frames cleared" event might
         // fire from the conditional expression evaluation if it's too
         // slow, which is what we wait for to reload the page)
         return resumeAndTestBreakpoint(30);
       })
       .then(() => resumeAndTestNoBreakpoint())
-      .then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
+      .then(() => {
+        return promise.all([
+          reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
+        ]);
+      })
       .then(() => testAfterReload())
       .then(() => {
         // Reset traits back to default value
         client.mainRoot.traits.conditionalBreakpoints = true;
       })
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-04-breakpoint.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-04-breakpoint.js
@@ -98,17 +98,18 @@ function afterDebuggerStatementHit() {
     "The currently shown source is incorrect (4).");
   ok(isCaretPos(gPanel, 6),
     "The source editor caret position is incorrect (4).");
 
   promise.all([
     waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE),
     waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCES_ADDED),
     waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN),
-    reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN)
+    reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR),
+    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE)
   ]).then(testClickAgain);
 }
 
 function testClickAgain() {
   isnot(gDebugger.gThreadClient.state, "paused",
     "The breakpoint wasn't hit yet (5).");
   is(gSources.selectedValue, SOURCE_URL,
     "The currently shown source is incorrect (5).");
--- a/browser/devtools/debugger/test/browser_dbg_server-conditional-bp-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_server-conditional-bp-01.js
@@ -33,17 +33,22 @@ function test() {
       .then(() => resumeAndTestBreakpoint(22))
       .then(() => resumeAndTestBreakpoint(23))
       .then(() => resumeAndTestBreakpoint(24))
       .then(() => resumeAndTestBreakpoint(25))
       .then(() => resumeAndTestBreakpoint(27))
       .then(() => resumeAndTestBreakpoint(28))
       .then(() => resumeAndTestBreakpoint(29))
       .then(() => resumeAndTestNoBreakpoint())
-      .then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
+      .then(() => {
+        return promise.all([
+          reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
+          waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
+        ]);
+      })
       .then(() => testAfterReload())
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
 
     gDebuggee.ermahgerd();
   });
--- a/browser/devtools/debugger/test/browser_dbg_stack-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-03.js
@@ -14,38 +14,40 @@ function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gFrames = gDebugger.DebuggerView.StackFrames;
     gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
-    waitForSourceAndCaretAndScopes(gPanel, ".html", 26).then(performTest);
+
+    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
+      .then(performTest);
 
     gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
     gDebuggee.recurse();
   });
 }
 
 function performTest() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, gDebugger.gCallStackPageSize,
     "Should have only the max limit of frames.");
   is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize,
-    "Should have only the max limit of frames in the mirrored view as well.")
+    "Should have only the max limit of frames in the mirrored view as well.");
 
-  gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
+  waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
     is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2,
       "Should now have twice the max limit of frames.");
     is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
       "Should now have twice the max limit of frames in the mirrored view as well.");
 
-    gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
+    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
       is(gFrames.itemCount, gDebuggee.gRecurseLimit,
         "Should have reached the recurse limit.");
       is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit,
         "Should have reached the recurse limit in the mirrored view as well.");
 
       gDebugger.gThreadClient.resume(() => {
         window.clearInterval(gFramesScrollingInterval);
         closeDebuggerAndFinish(gPanel);
--- a/browser/devtools/debugger/test/browser_dbg_watch-expressions-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_watch-expressions-02.js
@@ -20,19 +20,19 @@ function test() {
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gWatch = gDebugger.DebuggerView.WatchExpressions;
     gVariables = gDebugger.DebuggerView.Variables;
 
     gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
 
     waitForSourceShown(gPanel, ".html", 1)
-      .then(() => addExpressions())
-      .then(() => performTest())
-      .then(() => finishTest())
+      .then(addExpressions)
+      .then(performTest)
+      .then(finishTest)
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 
   function addExpressions() {
     gWatch.addExpression("'a'");
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -6,22 +6,29 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const DBG_XUL = "chrome://browser/content/devtools/framework/toolbox-process-window.xul";
 const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm")
+
+XPCOMUtils.defineLazyModuleGetter(this, "DevToolsLoader",
+  "resource://gre/modules/devtools/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+  "resource://gre/modules/devtools/Loader.jsm");
 
-Cu.import("resource://gre/modules/devtools/Loader.jsm");
-let require = devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-let EventEmitter = require("devtools/toolkit/event-emitter");
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
+  return devtools.require("devtools/shared/telemetry");
+});
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
+  return devtools.require("devtools/toolkit/event-emitter");
+});
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
 
 let processes = Set();
 
 /**
  * Constructor for creating a process that will hold a chrome toolbox.
@@ -128,20 +135,22 @@ BrowserToolboxProcess.prototype = {
     }
 
     if (!this.debuggerServer.initialized) {
       this.debuggerServer.init();
       this.debuggerServer.addBrowserActors();
       dumpn("initialized and added the browser actors for the DebuggerServer.");
     }
 
-    this.debuggerServer.openListener(Prefs.chromeDebuggingPort);
+    let chromeDebuggingPort =
+      Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+    this.debuggerServer.openListener(chromeDebuggingPort);
 
     dumpn("Finished initializing the chrome toolbox server.");
-    dumpn("Started listening on port: " + Prefs.chromeDebuggingPort);
+    dumpn("Started listening on port: " + chromeDebuggingPort);
   },
 
   /**
    * Initializes a profile for the remote debugger process.
    */
   _initProfile: function() {
     dumpn("Initializing the chrome toolbox user profile.");
 
@@ -243,30 +252,24 @@ BrowserToolboxProcess.prototype = {
     dumpn("Chrome toolbox is now closed...");
     this.closed = true;
     this.emit("close", this);
     processes.delete(this);
   }
 };
 
 /**
- * Shortcuts for accessing various debugger preferences.
- */
-let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
-  chromeDebuggingHost: ["Char", "chrome-debugging-host"],
-  chromeDebuggingPort: ["Int", "chrome-debugging-port"]
-});
-
-/**
  * Helper method for debugging.
  * @param string
  */
 function dumpn(str) {
   if (wantLogging) {
     dump("DBG-FRONTEND: " + str + "\n");
   }
 }
 
 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 
 Services.prefs.addObserver("devtools.debugger.log", {
   observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
 }, false);
+
+Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -5,20 +5,20 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/devtools/event-emitter.js");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
 
+const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -44,17 +44,25 @@ function connect() {
         });
       });
     } else {
       gClient.listTabs(openToolbox);
     }
   });
 }
 
-window.addEventListener("load", connect);
+window.addEventListener("load", function() {
+  let cmdClose = document.getElementById("toolbox-cmd-close");
+  cmdClose.addEventListener("command", onCloseCommand);
+  connect();
+});
+
+function onCloseCommand(event) {
+  window.close();
+}
 
 function openToolbox(form) {
   let options = {
     form: form,
     client: gClient,
     chrome: true
   };
   devtools.TargetFactory.forRemoteTab(options).then(target => {
@@ -77,16 +85,18 @@ function onNewToolbox(toolbox) {
 function bindToolboxHandlers() {
   gToolbox.once("destroyed", quitApp);
   window.addEventListener("unload", onUnload);
 }
 
 function onUnload() {
   window.removeEventListener("unload", onUnload);
   window.removeEventListener("message", onMessage);
+  let cmdClose = document.getElementById("toolbox-cmd-close");
+  cmdClose.removeEventListener("command", onCloseCommand);
   gToolbox.destroy();
 }
 
 function onMessage(event) {
   try {
     let json = JSON.parse(event.data);
     switch (json.name) {
       case "toolbox-raise":
--- a/browser/devtools/framework/toolbox-process-window.xul
+++ b/browser/devtools/framework/toolbox-process-window.xul
@@ -18,17 +18,17 @@
         persist="screenX screenY width height sizemode">
 
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="text/javascript" src="toolbox-process-window.js"/>
   <script type="text/javascript" src="chrome://global/content/viewSourceUtils.js"/>
   <script type="text/javascript" src="chrome://browser/content/utilityOverlay.js"/>
 
   <commandset id="toolbox-commandset">
-    <command id="toolbox-cmd-close" oncommand="window.close();"/>
+    <command id="toolbox-cmd-close"/>
   </commandset>
 
   <keyset id="toolbox-keyset">
     <key id="toolbox-key-close"
          key="&closeCmd.key;"
          command="toolbox-cmd-close"
          modifiers="accel"/>
   </keyset>
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -2,16 +2,21 @@
 # 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/.
 
 browser.jar:
     content/browser/devtools/widgets.css                               (shared/widgets/widgets.css)
     content/browser/devtools/widgets/VariablesView.xul                 (shared/widgets/VariablesView.xul)
     content/browser/devtools/markup-view.xhtml                         (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css                           (markupview/markup-view.css)
+    content/browser/devtools/projecteditor.xul                               (projecteditor/chrome/content/projecteditor.xul)
+    content/browser/devtools/readdir.js                                (projecteditor/lib/helpers/readdir.js)
+    content/browser/devtools/projecteditor-loader.xul                        (projecteditor/chrome/content/projecteditor-loader.xul)
+    content/browser/devtools/projecteditor-test.html                         (projecteditor/chrome/content/projecteditor-test.html)
+    content/browser/devtools/projecteditor-loader.js                         (projecteditor/chrome/content/projecteditor-loader.js)
     content/browser/devtools/netmonitor.xul                            (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css                            (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js                  (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js                        (netmonitor/netmonitor-view.js)
     content/browser/devtools/NetworkPanel.xhtml                        (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
 *   content/browser/devtools/scratchpad.xul                            (scratchpad/scratchpad.xul)
     content/browser/devtools/scratchpad.js                             (scratchpad/scratchpad.js)
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -8,16 +8,17 @@ DIRS += [
     'app-manager',
     'canvasdebugger',
     'commandline',
     'debugger',
     'eyedropper',
     'fontinspector',
     'framework',
     'inspector',
+    'projecteditor',
     'layoutview',
     'markupview',
     'netmonitor',
     'profiler',
     'responsivedesign',
     'scratchpad',
     'shadereditor',
     'shared',
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/Makefile.in
@@ -0,0 +1,14 @@
+# 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/.
+
+projecteditor_lib_FILES = $(wildcard $(srcdir)/lib/*)
+projecteditor_lib_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor
+INSTALL_TARGETS += projecteditor_lib
+
+# To copy the sample directory into modules/devtools/projecteditor
+# projecteditor_sample_FILES = $(wildcard $(srcdir)/test/samples/*)
+# projecteditor_sample_DEST = $(FINAL_TARGET)/modules/devtools/projecteditor/samples
+# INSTALL_TARGETS += projecteditor_sample
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js
@@ -0,0 +1,157 @@
+const Cu = Components.utils;
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const require = devtools.require;
+const promise = require("projecteditor/helpers/promise");
+const ProjectEditor = require("projecteditor/projecteditor");
+
+const SAMPLE_PATH = buildTempDirectoryStructure();
+const SAMPLE_NAME = "DevTools Content";
+const SAMPLE_PROJECT_URL = "http://mozilla.org";
+const SAMPLE_ICON = "chrome://browser/skin/devtools/tool-options.svg";
+
+/**
+ * Create a workspace for working on projecteditor, available at
+ * chrome://browser/content/devtools/projecteditor-loader.xul.
+ * This emulates the integration points that the app manager uses.
+ */
+document.addEventListener("DOMContentLoaded", function onDOMReady(e) {
+  document.removeEventListener("DOMContentLoaded", onDOMReady, false);
+  let iframe = document.getElementById("projecteditor-iframe");
+  window.projecteditor = ProjectEditor.ProjectEditor(iframe);
+
+  projecteditor.on("onEditorCreated", (editor) => {
+    console.log("editor created: " + editor);
+  });
+  projecteditor.on("onEditorDestroyed", (editor) => {
+    console.log("editor destroyed: " + editor);
+  });
+  projecteditor.on("onEditorSave", (editor, resource) => {
+    console.log("editor saved: " + editor, resource.path);
+  });
+  projecteditor.on("onTreeSelected", (resource) => {
+    console.log("tree selected: " + resource.path);
+  });
+  projecteditor.on("onEditorLoad", (editor) => {
+    console.log("editor loaded: " + editor);
+  });
+  projecteditor.on("onEditorActivated", (editor) => {
+    console.log("editor focused: " + editor);
+  });
+  projecteditor.on("onEditorDeactivated", (editor) => {
+    console.log("editor blur: " + editor);
+  });
+  projecteditor.on("onEditorChange", (editor) => {
+    console.log("editor changed: " + editor);
+  });
+  projecteditor.on("onEditorCursorActivity", (editor) => {
+    console.log("editor cursor activity: " + editor);
+  });
+  projecteditor.on("onCommand", (cmd) => {
+    console.log("Command: " + cmd);
+  });
+
+  projecteditor.loaded.then(() => {
+    projecteditor.setProjectToAppPath(SAMPLE_PATH, {
+      name: SAMPLE_NAME,
+      iconUrl: SAMPLE_ICON,
+      projectOverviewURL: SAMPLE_PROJECT_URL
+    }).then(() => {
+      let allResources = projecteditor.project.allResources();
+      console.log("All resources have been loaded", allResources, allResources.map(r=>r.basename).join("|"));
+    });
+  });
+
+}, false);
+
+/**
+ * Build a temporary directory as a workspace for this loader
+ * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
+ */
+function buildTempDirectoryStructure() {
+
+  // First create (and remove) the temp dir to discard any changes
+  let TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
+  TEMP_DIR.remove(true);
+
+  // Now rebuild our fake project.
+  TEMP_DIR = FileUtils.getDir("TmpD", ["ProjectEditor"], true);
+
+  FileUtils.getDir("TmpD", ["ProjectEditor", "css"], true);
+  FileUtils.getDir("TmpD", ["ProjectEditor", "data"], true);
+  FileUtils.getDir("TmpD", ["ProjectEditor", "img", "icons"], true);
+  FileUtils.getDir("TmpD", ["ProjectEditor", "js"], true);
+
+  let htmlFile = FileUtils.getFile("TmpD", ["ProjectEditor", "index.html"]);
+  htmlFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  writeToFile(htmlFile, [
+    '<!DOCTYPE html>',
+    '<html lang="en">',
+    ' <head>',
+    '   <meta charset="utf-8" />',
+    '   <title>ProjectEditor Temp File</title>',
+    '   <link rel="stylesheet" href="style.css" />',
+    ' </head>',
+    ' <body id="home">',
+    '   <p>ProjectEditor Temp File</p>',
+    ' </body>',
+    '</html>'].join("\n")
+  );
+
+  let readmeFile = FileUtils.getFile("TmpD", ["ProjectEditor", "README.md"]);
+  readmeFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  writeToFile(readmeFile, [
+    '## Readme'
+    ].join("\n")
+  );
+
+  let licenseFile = FileUtils.getFile("TmpD", ["ProjectEditor", "LICENSE"]);
+  licenseFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  writeToFile(licenseFile, [
+   '/* 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/. */'
+    ].join("\n")
+  );
+
+  let cssFile = FileUtils.getFile("TmpD", ["ProjectEditor", "css", "styles.css"]);
+  cssFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  writeToFile(cssFile, [
+    'body {',
+    ' background: red;',
+    '}'
+    ].join("\n")
+  );
+
+  FileUtils.getFile("TmpD", ["ProjectEditor", "js", "script.js"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+  FileUtils.getFile("TmpD", ["ProjectEditor", "img", "fake.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "16x16.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "32x32.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "128x128.png"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+  FileUtils.getFile("TmpD", ["ProjectEditor", "img", "icons", "vector.svg"]).createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+  return TEMP_DIR.path;
+}
+
+
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O#Writing_to_a_file
+function writeToFile(file, data) {
+
+  let defer = promise.defer();
+  var ostream = FileUtils.openSafeFileOutputStream(file)
+
+  var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
+                  createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  var istream = converter.convertToInputStream(data);
+
+  // The last argument (the callback) is optional.
+  NetUtil.asyncCopy(istream, ostream, function(status) {
+    if (!Components.isSuccessCode(status)) {
+      // Handle error!
+      console.log("ERROR WRITING TEMP FILE", status);
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor-loader.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://browser/locale/devtools/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/javascript;version=1.8" src="projecteditor-loader.js"></script>
+
+  <commandset id="toolbox-commandset">
+    <command id="projecteditor-cmd-close" oncommand="window.close();"/>
+  </commandset>
+
+  <keyset id="projecteditor-keyset">
+    <key id="projecteditor-key-close"
+         key="&closeCmd.key;"
+         command="projecteditor-cmd-close"
+         modifiers="accel"/>
+  </keyset>
+
+  <iframe id="projecteditor-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
+</window>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor-test.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<head>
+  <meta charset='utf-8' />
+</head>
+<body>
+  <style type="text/css">
+    html { height: 100%; }
+    body {display: flex; padding: 0; margin: 0; min-height: 100%; }
+    iframe {flex: 1; border: 0;}
+  </style>
+  <iframe id='projecteditor-iframe'></iframe>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor.xul
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://browser/skin/devtools/light-theme.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/projecteditor/projecteditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/markup-view.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/markup-view.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
+ %scratchpadDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuStrings;
+<!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
+%sourceEditorStrings;
+]>
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="theme-body">
+
+  <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+  <commandset id="projecteditor-commandset" />
+  <commandset id="editMenuCommands"/>
+  <keyset id="projecteditor-keyset" />
+  <keyset id="editMenuKeys"/>
+
+  <!-- Eventually we want to let plugins declare their own menu items.
+       Wait unti app manager lands to deal with this integration point.
+  -->
+  <menubar id="projecteditor-menubar">
+    <menu id="file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+      <menupopup id="file-menu-popup" />
+    </menu>
+
+    <menu id="edit-menu" label="&editMenu.label;"
+          accesskey="&editMenu.accesskey;">
+      <menupopup id="edit-menu-popup">
+        <menuitem id="menu_undo"/>
+        <menuitem id="menu_redo"/>
+        <menuseparator/>
+        <menuitem id="menu_cut"/>
+        <menuitem id="menu_copy"/>
+        <menuitem id="menu_paste"/>
+        <menuseparator/>
+        <menuitem id="menu_selectAll"/>
+        <menuseparator/>
+        <menuitem id="menu_find"/>
+        <menuitem id="menu_findAgain"/>
+      </menupopup>
+    </menu>
+  </menubar>
+
+
+  <popupset>
+    <menupopup id="directory-menu-popup">
+    </menupopup>
+  </popupset>
+
+  <deck id="main-deck" flex="1">
+    <vbox flex="1" id="source-deckitem">
+      <hbox id="sources-body" flex="1">
+        <vbox width="250">
+          <vbox id="sources" flex="1">
+          </vbox>
+          <toolbar id="project-toolbar" class="devtools-toolbar" hidden="true"></toolbar>
+        </vbox>
+        <splitter id="source-editor-splitter" class="devtools-side-splitter"/>
+        <vbox id="shells" flex="4">
+          <toolbar id="projecteditor-toolbar" class="devtools-toolbar">
+            <hbox id="plugin-toolbar-left"/>
+            <spacer flex="1"/>
+            <hbox id="plugin-toolbar-right"/>
+          </toolbar>
+          <box id="shells-deck-container" flex="4"></box>
+          <toolbar id="projecteditor-toolbar-bottom" class="devtools-toolbar">
+          </toolbar>
+        </vbox>
+      </hbox>
+    </vbox>
+  </deck>
+</page>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/editors.js
@@ -0,0 +1,263 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("projecteditor/helpers/promise");
+const Editor  = require("devtools/sourceeditor/editor");
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * ItchEditor is extended to implement an editor, which is the main view
+ * that shows up when a file is selected.  This object should not be used
+ * directly - use TextEditor for a basic code editor.
+ */
+var ItchEditor = Class({
+  extends: EventTarget,
+
+  /**
+   * A boolean specifying if the toolbar above the editor should be hidden.
+   */
+  hidesToolbar: false,
+
+  toString: function() {
+    return this.label || "";
+  },
+
+  emit: function(name, ...args) {
+    emit(this, name, ...args);
+  },
+
+  /**
+   * Initialize the editor with a single document.  This should be called
+   * by objects extending this object with:
+   * ItchEditor.prototype.initialize.apply(this, arguments)
+   */
+  initialize: function(document) {
+    this.doc = document;
+    this.label = "";
+    this.elt = this.doc.createElement("vbox");
+    this.elt.setAttribute("flex", "1");
+    this.elt.editor = this;
+    this.toolbar = this.doc.querySelector("#projecteditor-toolbar");
+  },
+
+  /**
+   * Sets the visibility of the element that shows up above the editor
+   * based on the this.hidesToolbar property.
+   */
+  setToolbarVisibility: function() {
+    if (this.hidesToolbar) {
+      this.toolbar.setAttribute("hidden", "true");
+    } else {
+      this.toolbar.removeAttribute("hidden");
+    }
+  },
+
+
+  /**
+   * Load a single resource into the editor.
+   *
+   * @param Resource resource
+   *        The single file / item that is being dealt with (see stores/base)
+   * @returns Promise
+   *          A promise that is resolved once the editor has loaded the contents
+   *          of the resource.
+   */
+  load: function(resource) {
+    return promise.resolve();
+  },
+
+  /**
+   * Clean up the editor.  This can have different meanings
+   * depending on the type of editor.
+   */
+  destroy: function() {
+
+  },
+
+  /**
+   * Give focus to the editor.  This can have different meanings
+   * depending on the type of editor.
+   *
+   * @returns Promise
+   *          A promise that is resolved once the editor has been focused.
+   */
+  focus: function() {
+    return promise.resolve();
+  }
+});
+exports.ItchEditor = ItchEditor;
+
+/**
+ * The main implementation of the ItchEditor class.  The TextEditor is used
+ * when editing any sort of plain text file, and can be created with different
+ * modes for syntax highlighting depending on the language.
+ */
+var TextEditor = Class({
+  extends: ItchEditor,
+
+  /**
+   * Extra keyboard shortcuts to use with the editor.  Shortcuts defined
+   * within projecteditor should be triggered when they happen in the editor, and
+   * they would usually be swallowed without registering them.
+   * See "devtools/sourceeditor/editor" for more information.
+   */
+  get extraKeys() {
+    let extraKeys = {};
+
+    // Copy all of the registered keys into extraKeys object, to notify CodeMirror
+    // that it should be ignoring these keys
+    [...this.doc.querySelectorAll("#projecteditor-keyset key")].forEach((key) => {
+      let keyUpper = key.getAttribute("key").toUpperCase();
+      let toolModifiers = key.getAttribute("modifiers");
+      let modifiers = {
+        alt: toolModifiers.contains("alt"),
+        shift: toolModifiers.contains("shift")
+      };
+
+      // On the key press, we will dispatch the event within projecteditor.
+      extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
+        let event = this.doc.createEvent('Event');
+        event.initEvent('command', true, true);
+        let command = this.doc.querySelector("#" + key.getAttribute("command"));
+        command.dispatchEvent(event);
+      };
+    });
+
+    return extraKeys;
+  },
+
+  initialize: function(document, mode=Editor.modes.text) {
+    ItchEditor.prototype.initialize.apply(this, arguments);
+    this.label = mode.name;
+    this.editor = new Editor({
+      mode: mode,
+      lineNumbers: true,
+      extraKeys: this.extraKeys,
+      themeSwitching: false
+    });
+
+    // Trigger editor specific events on `this`
+    this.editor.on("change", (...args) => {
+      this.emit("change", ...args);
+    });
+    this.editor.on("cursorActivity", (...args) => {
+      this.emit("cursorActivity", ...args);
+    });
+
+    this.appended = this.editor.appendTo(this.elt);
+  },
+
+  /**
+   * Clean up the editor.  This can have different meanings
+   * depending on the type of editor.
+   */
+  destroy: function() {
+    this.editor.destroy();
+    this.editor = null;
+  },
+
+  /**
+   * Load a single resource into the text editor.
+   *
+   * @param Resource resource
+   *        The single file / item that is being dealt with (see stores/base)
+   * @returns Promise
+   *          A promise that is resolved once the text editor has loaded the
+   *          contents of the resource.
+   */
+  load: function(resource) {
+    // Wait for the editor.appendTo and resource.load before proceeding.
+    // They can run  in parallel.
+    return promise.all([
+      resource.load(),
+      this.appended
+    ]).then(([resourceContents])=> {
+      this.editor.setText(resourceContents);
+      this.editor.setClean();
+      this.emit("load");
+    }, console.error);
+  },
+
+  /**
+   * Save the resource based on the current state of the editor
+   *
+   * @param Resource resource
+   *        The single file / item to be saved
+   * @returns Promise
+   *          A promise that is resolved once the resource has been
+   *          saved.
+   */
+  save: function(resource) {
+    return resource.save(this.editor.getText()).then(() => {
+      this.editor.setClean();
+      this.emit("save", resource);
+    });
+  },
+
+  /**
+   * Give focus to the code editor.
+   *
+   * @returns Promise
+   *          A promise that is resolved once the editor has been focused.
+   */
+  focus: function() {
+    return this.appended.then(() => {
+      this.editor.focus();
+    });
+  }
+});
+
+/**
+ * Wrapper for TextEditor using JavaScript syntax highlighting.
+ */
+function JSEditor(document) {
+  return TextEditor(document, Editor.modes.js);
+}
+
+/**
+ * Wrapper for TextEditor using CSS syntax highlighting.
+ */
+function CSSEditor(document) {
+  return TextEditor(document, Editor.modes.css);
+}
+
+/**
+ * Wrapper for TextEditor using HTML syntax highlighting.
+ */
+function HTMLEditor(document) {
+  return TextEditor(document, Editor.modes.html);
+}
+
+/**
+ * Get the type of editor that can handle a particular resource.
+ * @param Resource resource
+ *        The single file that is going to be opened.
+ * @returns Type:Editor
+ *          The type of editor that can handle this resource.  The
+ *          return value is a constructor function.
+ */
+function EditorTypeForResource(resource) {
+  const categoryMap = {
+    "txt": TextEditor,
+    "html": HTMLEditor,
+    "xml": HTMLEditor,
+    "css": CSSEditor,
+    "js": JSEditor,
+    "json": JSEditor
+  };
+  return categoryMap[resource.contentCategory] || TextEditor;
+}
+
+exports.TextEditor = TextEditor;
+exports.JSEditor = JSEditor;
+exports.CSSEditor = CSSEditor;
+exports.HTMLEditor = HTMLEditor;
+exports.EditorTypeForResource = EditorTypeForResource;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/helpers/event.js
@@ -0,0 +1,86 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+/**
+ * This file wraps EventEmitter objects to provide functions to forget
+ * all events bound on a certain object.
+ */
+
+const { Class } = require("sdk/core/heritage");
+
+/**
+ * The Scope object is used to keep track of listeners.
+ * This object is not exported.
+ */
+var Scope = Class({
+  on: function(target, event, handler) {
+    this.listeners = this.listeners || [];
+    this.listeners.push({
+      target: target,
+      event: event,
+      handler: handler
+    });
+    target.on(event, handler);
+  },
+
+  off: function(t, e, h) {
+    if (!this.listeners) return;
+    this.listeners = this.listeners.filter(({ target, event, handler }) => {
+      return !(target === t && event === e && handler === h);
+    });
+    target.off(event, handler);
+  },
+
+  clear: function(clearTarget) {
+    if (!this.listeners) return;
+    this.listeners = this.listeners.filter(({ target, event, handler }) => {
+      if (target === clearTarget) {
+        target.off(event, handler);
+        return false;
+      }
+      return true;
+    });
+  },
+
+  destroy: function() {
+    if (!this.listeners) return;
+    this.listeners.forEach(({ target, event, handler }) => {
+      target.off(event, handler);
+    });
+    this.listeners = undefined;
+  }
+});
+
+var scopes = new WeakMap();
+function scope(owner) {
+  if (!scopes.has(owner)) {
+    let scope = new Scope(owner);
+    scopes.set(owner, scope);
+    return scope;
+  }
+  return scopes.get(owner);
+}
+exports.scope = scope;
+
+exports.on = function on(owner, target, event, handler) {
+  if (!target) return;
+  scope(owner).on(target, event, handler);
+}
+
+exports.off = function off(owner, target, event, handler) {
+  if (!target) return;
+  scope(owner).off(target, event, handler);
+}
+
+exports.forget = function forget(owner, target) {
+  scope(owner).clear(target);
+}
+
+exports.done = function done(owner) {
+  scope(owner).destroy();
+  scopes.delete(owner);
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/helpers/file-picker.js
@@ -0,0 +1,116 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+/**
+ * This file contains helper functions for showing OS-specific
+ * file and folder pickers.
+ */
+
+const { Cu, Cc, Ci } = require("chrome");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const promise = require("projecteditor/helpers/promise");
+const { merge } = require("sdk/util/object");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+/**
+ * Show a file / folder picker.
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
+ *
+ * @param object options
+ *        Additional options for setting the source. Supported options:
+ *          - directory: string, The path to default opening
+ *          - defaultName: string, The filename including extension that
+ *                         should be suggested to the user as a default
+ *          - window: DOMWindow, The filename including extension that
+ *                         should be suggested to the user as a default
+ *          - title: string, The filename including extension that
+ *                         should be suggested to the user as a default
+ *          - mode: int, The type of picker to open.
+ *
+ * @return promise
+ *         A promise that is resolved with the full path
+ *         after the file has been picked.
+ */
+function showPicker(options) {
+  let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+  if (options.directory) {
+    try {
+      fp.displayDirectory = FileUtils.File(options.directory);
+    } catch(ex) {
+      console.warn(ex);
+    }
+  }
+
+  if (options.defaultName) {
+    fp.defaultString = options.defaultName;
+  }
+
+  fp.init(options.window, options.title, options.mode);
+  let deferred = promise.defer();
+  fp.open({
+    done: function(res) {
+      if (res === Ci.nsIFilePicker.returnOK || res === Ci.nsIFilePicker.returnReplace) {
+        deferred.resolve(fp.file.path);
+      } else {
+        deferred.reject();
+      }
+    }
+  });
+  return deferred.promise;
+}
+exports.showPicker = showPicker;
+
+/**
+ * Show a save dialog
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker
+ *
+ * @param object options
+ *        Additional options as specified in showPicker
+ *
+ * @return promise
+ *         A promise that is resolved when the save dialog has closed
+ */
+function showSave(options) {
+  return showPicker(merge({
+    title: getLocalizedString("projecteditor.selectFileLabel"),
+    mode: Ci.nsIFilePicker.modeSave
+  }, options));
+}
+exports.showSave = showSave;
+
+/**
+ * Show a file open dialog
+ *
+ * @param object options
+ *        Additional options as specified in showPicker
+ *
+ * @return promise
+ *         A promise that is resolved when the file has been opened
+ */
+function showOpen(options) {
+  return showPicker(merge({
+    title: getLocalizedString("projecteditor.openFileLabel"),
+    mode: Ci.nsIFilePicker.modeOpen
+  }, options));
+}
+exports.showOpen = showOpen;
+
+/**
+ * Show a folder open dialog
+ *
+ * @param object options
+ *        Additional options as specified in showPicker
+ *
+ * @return promise
+ *         A promise that is resolved when the folder has been opened
+ */
+function showOpenFolder(options) {
+  return showPicker(merge({
+    title: getLocalizedString("projecteditor.openFolderLabel"),
+    mode: Ci.nsIFilePicker.modeGetFolder
+  }, options));
+}
+exports.showOpenFolder = showOpenFolder;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/helpers/l10n.js
@@ -0,0 +1,25 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+/**
+ * This file contains helper functions for internationalizing projecteditor strings
+ */
+
+const { Cu, Cc, Ci } = require("chrome");
+const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+const ITCHPAD_STRINGS_URI = "chrome://browser/locale/devtools/projecteditor.properties";
+const L10N = new ViewHelpers.L10N(ITCHPAD_STRINGS_URI).stringBundle;
+
+function getLocalizedString (name) {
+  try {
+    return L10N.GetStringFromName(name);
+  } catch (ex) {
+    console.log("Error reading '" + name + "'");
+    throw new Error("l10n error with " + name);
+  }
+}
+
+exports.getLocalizedString = getLocalizedString;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/helpers/promise.js
@@ -0,0 +1,11 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+/**
+ * This helper is a quick way to require() the Promise object from Promise.jsm.
+ */
+const { Cu } = require("chrome");
+module.exports = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/helpers/readdir.js
@@ -0,0 +1,89 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+importScripts("resource://gre/modules/osfile.jsm");
+
+/**
+ * This file is meant to be loaded in a worker using:
+ *   new ChromeWorker("chrome://browser/content/devtools/readdir.js");
+ *
+ * Read a local directory inside of a web woker
+ *
+ * @param {string} path
+ *        window to inspect
+ * @param {RegExp|string} ignore
+ *        A pattern to ignore certain files.  This is
+ *        called with file.name.match(ignore).
+ * @param {Number} maxDepth
+ *        How many directories to recurse before stopping.
+ *        Directories with depth > maxDepth will be ignored.
+ */
+function readDir(path, ignore, maxDepth = Infinity) {
+  let ret = {};
+
+  let set = new Set();
+
+  let info = OS.File.stat(path);
+  set.add({
+    path: path,
+    name: info.name,
+    isDir: info.isDir,
+    isSymLink: info.isSymLink,
+    depth: 0
+  });
+
+  for (let info of set) {
+    let children = [];
+
+    if (info.isDir && !info.isSymLink) {
+      if (info.depth > maxDepth) {
+        continue;
+      }
+
+      let iterator = new OS.File.DirectoryIterator(info.path);
+      try {
+        for (let child in iterator) {
+          if (ignore && child.name.match(ignore)) {
+            continue;
+          }
+
+          children.push(child.path);
+          set.add({
+            path: child.path,
+            name: child.name,
+            isDir: child.isDir,
+            isSymLink: child.isSymLink,
+            depth: info.depth + 1
+          });
+        }
+      } finally {
+        iterator.close();
+      }
+    }
+
+    ret[info.path] = {
+      name: info.name,
+      isDir: info.isDir,
+      isSymLink: info.isSymLink,
+      depth: info.depth,
+      children: children,
+    };
+  }
+
+  return ret;
+};
+
+onmessage = function (event) {
+  try {
+    let {path, ignore, depth} = event.data;
+    let message = readDir(path, ignore, depth);
+    postMessage(message);
+  } catch(ex) {
+    console.log(ex);
+  }
+};
+
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/app-manager/lib/app-project-editor.js
@@ -0,0 +1,44 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("projecteditor/helpers/promise");
+const { ItchEditor } = require("projecteditor/editors");
+
+var AppProjectEditor = Class({
+  extends: ItchEditor,
+
+  hidesToolbar: true,
+
+  initialize: function(document, host) {
+    ItchEditor.prototype.initialize.apply(this, arguments);
+    this.appended = promise.resolve();
+    this.host = host;
+    this.label = "app-manager";
+  },
+
+  destroy: function() {
+    this.elt.remove();
+    this.elt = null;
+  },
+
+  load: function(resource) {
+    this.elt.textContent = "";
+    let {appManagerOpts} = this.host.project;
+    let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
+    iframe.setAttribute("flex", "1");
+    iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
+    this.elt.appendChild(iframe);
+
+    // Wait for other `appended` listeners before emitting load.
+    this.appended.then(() => {
+      this.emit("load");
+    });
+  }
+});
+
+exports.AppProjectEditor = AppProjectEditor;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/app-manager/lib/plugin.js
@@ -0,0 +1,47 @@
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("projecteditor/helpers/promise");
+var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { AppProjectEditor } = require("./app-project-editor");
+
+var AppManagerRenderer = Class({
+  extends: Plugin,
+
+  isAppManagerProject: function() {
+    return !!this.host.project.appManagerOpts;
+  },
+  editorForResource: function(resource) {
+    if (!resource.parent && this.isAppManagerProject()) {
+      return AppProjectEditor;
+    }
+  },
+  onAnnotate: function(resource, editor, elt) {
+    if (resource.parent || !this.isAppManagerProject()) {
+      return;
+    }
+
+    let {appManagerOpts} = this.host.project;
+    let doc = elt.ownerDocument;
+    let image = doc.createElement("image");
+    let label = doc.createElement("label");
+
+    label.className = "project-name-label";
+    image.className = "project-image";
+
+    let name = appManagerOpts.name || resource.basename;
+    let url = appManagerOpts.iconUrl || "icon-sample.png";
+
+    label.textContent = name;
+    image.setAttribute("src", url);
+
+    elt.innerHTML = "";
+    elt.appendChild(image);
+    elt.appendChild(label);
+    return true;
+  }
+});
+
+exports.AppManagerRenderer = AppManagerRenderer;
+registerPlugin(AppManagerRenderer);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/core.js
@@ -0,0 +1,83 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+// This is the core plugin API.
+
+const { Class } = require("sdk/core/heritage");
+
+var Plugin = Class({
+  initialize: function(host) {
+    this.host = host;
+    this.init(host);
+  },
+
+  destroy: function(host) { },
+
+  init: function(host) {},
+
+  showForCategories: function(elt, categories) {
+    this._showFor = this._showFor || [];
+    let set = new Set(categories);
+    this._showFor.push({
+      elt: elt,
+      categories: new Set(categories)
+    });
+    if (this.host.currentEditor) {
+      this.onEditorActivated(this.host.currentEditor);
+    } else {
+      elt.classList.add("plugin-hidden");
+    }
+  },
+
+  priv: function(item) {
+    if (!this._privData) {
+      this._privData = new WeakMap();
+    }
+    if (!this._privData.has(item)) {
+       this._privData.set(item, {});
+    }
+    return this._privData.get(item);
+  },
+  onTreeSelected: function(resource) {},
+
+
+  // Editor state lifetime...
+  onEditorCreated: function(editor) {},
+  onEditorDestroyed: function(editor) {},
+
+  onEditorActivated: function(editor) {
+    if (this._showFor) {
+      let category = editor.category;
+      for (let item of this._showFor) {
+        if (item.categories.has(category)) {
+          item.elt.classList.remove("plugin-hidden");
+        } else {
+          item.elt.classList.add("plugin-hidden");
+        }
+      }
+    }
+  },
+  onEditorDeactivated: function(editor) {
+    if (this._showFor) {
+      for (let item of this._showFor) {
+        item.elt.classList.add("plugin-hidden");
+      }
+    }
+  },
+
+  onEditorLoad: function(editor) {},
+  onEditorSave: function(editor) {},
+  onEditorChange: function(editor) {},
+  onEditorCursorActivity: function(editor) {},
+});
+exports.Plugin = Plugin;
+
+function registerPlugin(constr) {
+  exports.registeredPlugins.push(constr);
+}
+exports.registerPlugin = registerPlugin;
+
+exports.registeredPlugins = [];
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/delete/lib/delete.js
@@ -0,0 +1,38 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+var DeletePlugin = Class({
+  extends: Plugin,
+
+  init: function(host) {
+    this.host.addCommand({
+      id: "cmd-delete"
+    });
+    this.host.createMenuItem({
+      parent: "#directory-menu-popup",
+      label: getLocalizedString("projecteditor.deleteLabel"),
+      command: "cmd-delete"
+    });
+  },
+
+  onCommand: function(cmd) {
+    if (cmd === "cmd-delete") {
+      let tree = this.host.projectTree;
+      let resource = tree.getSelectedResource();
+      let parent = resource.parent;
+      tree.deleteResource(resource).then(() => {
+        this.host.project.refresh();
+      })
+    }
+  }
+});
+
+exports.DeletePlugin = DeletePlugin;
+registerPlugin(DeletePlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/dirty/lib/dirty.js
@@ -0,0 +1,43 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { emit } = require("sdk/event/core");
+
+var DirtyPlugin = Class({
+  extends: Plugin,
+
+  onEditorSave: function(editor) { this.onEditorChange(editor); },
+  onEditorLoad: function(editor) { this.onEditorChange(editor); },
+
+  onEditorChange: function(editor) {
+    // Only run on a TextEditor
+    if (!editor || !editor.editor) {
+      return;
+    }
+
+    // Dont' force a refresh unless the dirty state has changed...
+    let priv = this.priv(editor);
+    let clean = editor.editor.isClean();
+    if (priv.isClean !== clean) {
+
+      let resource = editor.shell.resource;
+      emit(resource, "label-change", resource);
+      priv.isClean = clean;
+    }
+  },
+
+  onAnnotate: function(resource, editor, elt) {
+    if (editor && editor.editor && !editor.editor.isClean()) {
+      elt.textContent = '*' + resource.displayName;
+      return true;
+    }
+  }
+});
+exports.DirtyPlugin = DirtyPlugin;
+
+registerPlugin(DirtyPlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/image-view/lib/image-editor.js
@@ -0,0 +1,41 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("projecteditor/helpers/promise");
+const { ItchEditor } = require("projecteditor/editors");
+
+var ImageEditor = Class({
+  extends: ItchEditor,
+
+  initialize: function(document) {
+    ItchEditor.prototype.initialize.apply(this, arguments);
+    this.label = "image";
+    this.appended = promise.resolve();
+  },
+
+  load: function(resource) {
+    let image = this.doc.createElement("image");
+    image.className = "editor-image";
+    image.setAttribute("src", resource.uri);
+
+    let box1 = this.doc.createElement("box");
+    box1.appendChild(image);
+
+    let box2 = this.doc.createElement("box");
+    box2.setAttribute("flex", 1);
+
+    this.elt.appendChild(box1);
+    this.elt.appendChild(box2);
+
+    this.appended.then(() => {
+      this.emit("load");
+    });
+  }
+});
+
+exports.ImageEditor = ImageEditor;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/image-view/lib/plugin.js
@@ -0,0 +1,28 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("projecteditor/helpers/promise");
+const { ImageEditor } = require("./image-editor");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+
+var ImageEditorPlugin = Class({
+  extends: Plugin,
+
+  editorForResource: function(node) {
+    if (node.contentCategory === "image") {
+      return ImageEditor;
+    }
+  },
+
+  init: function(host) {
+
+  }
+});
+
+exports.ImageEditorPlugin = ImageEditorPlugin;
+registerPlugin(ImageEditorPlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/logging/lib/logging.js
@@ -0,0 +1,29 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+var { Class } = require("sdk/core/heritage");
+var { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+
+var LoggingPlugin = Class({
+  extends: Plugin,
+
+  // Editor state lifetime...
+  onEditorCreated: function(editor) { console.log("editor created: " + editor) },
+  onEditorDestroyed: function(editor) { console.log("editor destroyed: " + editor )},
+
+  onEditorSave: function(editor) { console.log("editor saved: " + editor) },
+  onEditorLoad: function(editor) { console.log("editor loaded: " + editor) },
+
+  onEditorActivated: function(editor) { console.log("editor activated: " + editor )},
+  onEditorDeactivated: function(editor) { console.log("editor deactivated: " + editor )},
+
+  onEditorChange: function(editor) { console.log("editor changed: " + editor )},
+
+  onCommand: function(cmd) { console.log("Command: " + cmd); }
+});
+exports.LoggingPlugin = LoggingPlugin;
+
+registerPlugin(LoggingPlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/new/lib/new.js
@@ -0,0 +1,90 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+// Handles the new command.
+var NewFile = Class({
+  extends: Plugin,
+
+  init: function(host) {
+    this.host.createMenuItem({
+      parent: "#file-menu-popup",
+      label: getLocalizedString("projecteditor.newLabel"),
+      command: "cmd-new",
+      key: "key-new"
+    });
+    this.host.createMenuItem({
+      parent: "#directory-menu-popup",
+      label: getLocalizedString("projecteditor.newLabel"),
+      command: "cmd-new"
+    });
+
+    this.command = this.host.addCommand({
+      id: "cmd-new",
+      key: getLocalizedString("projecteditor.new.commandkey"),
+      modifiers: "accel"
+    });
+  },
+
+  onCommand: function(cmd) {
+    if (cmd === "cmd-new") {
+      let tree = this.host.projectTree;
+      let resource = tree.getSelectedResource();
+      parent = resource.isDir ? resource : resource.parent;
+      sibling = resource.isDir ? null : resource;
+
+      if (!("createChild" in parent)) {
+        return;
+      }
+
+      let extension = sibling ? sibling.contentCategory : parent.store.defaultCategory;
+      let template = "untitled{1}." + extension;
+      let name = this.suggestName(parent, template);
+
+      tree.promptNew(name, parent, sibling).then(name => {
+
+        // XXX: sanitize bad file names.
+
+        // If the name is already taken, just add/increment a number.
+        if (this.hasChild(parent, name)) {
+          let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
+          template = matches[1] + "{1}" + matches[3] + matches[4];
+          name = this.suggestName(parent, template, parseInt(matches[2]) || 2);
+        }
+
+        return parent.createChild(name);
+      }).then(resource => {
+        tree.selectResource(resource);
+        this.host.currentEditor.focus();
+      }).then(null, console.error);
+    }
+  },
+
+  suggestName: function(parent, template, start=1) {
+    let i = start;
+    let name;
+    do {
+      name = template.replace("\{1\}", i === 1 ? "" : i);
+      i++;
+    } while (this.hasChild(parent, name));
+
+    return name;
+  },
+
+  hasChild: function(resource, name) {
+    for (let child of resource.children) {
+      if (child.basename === name) {
+        return true;
+      }
+    }
+    return false;
+  }
+})
+exports.NewFile = NewFile;
+registerPlugin(NewFile);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/save/lib/save.js
@@ -0,0 +1,89 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const picker = require("projecteditor/helpers/file-picker");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+// Handles the save command.
+var SavePlugin = Class({
+  extends: Plugin,
+
+  init: function(host) {
+
+    this.host.addCommand({
+      id: "cmd-saveas",
+      key: getLocalizedString("projecteditor.save.commandkey"),
+      modifiers: "accel shift"
+    });
+    this.host.addCommand({
+      id: "cmd-save",
+      key: getLocalizedString("projecteditor.save.commandkey"),
+      modifiers: "accel"
+    });
+
+    // Wait until we can add things into the app manager menu
+    // this.host.createMenuItem({
+    //   parent: "#file-menu-popup",
+    //   label: "Save",
+    //   command: "cmd-save",
+    //   key: "key-save"
+    // });
+    // this.host.createMenuItem({
+    //   parent: "#file-menu-popup",
+    //   label: "Save As",
+    //   command: "cmd-saveas",
+    // });
+  },
+
+  onCommand: function(cmd) {
+    if (cmd === "cmd-save") {
+      this.save();
+    } else if (cmd === "cmd-saveas") {
+      this.saveAs();
+    }
+  },
+
+  saveAs: function() {
+    let editor = this.host.currentEditor;
+    let project = this.host.resourceFor(editor);
+
+    let resource;
+    picker.showSave({
+      window: this.host.window,
+      directory: project && project.parent ? project.parent.path : null,
+      defaultName: project ? project.basename : null,
+    }).then(path => {
+      return this.createResource(path);
+    }).then(res => {
+      resource = res;
+      return this.saveResource(editor, resource);
+    }).then(() => {
+      this.host.openResource(resource);
+    }).then(null, console.error);
+  },
+
+  save: function() {
+    let editor = this.host.currentEditor;
+    let resource = this.host.resourceFor(editor);
+    if (!resource) {
+      return this.saveAs();
+    }
+
+    return this.saveResource(editor, resource);
+  },
+
+  createResource: function(path) {
+    return this.host.project.resourceFor(path, { create: true })
+  },
+
+  saveResource: function(editor, resource) {
+    return editor.save(resource);
+  }
+})
+exports.SavePlugin = SavePlugin;
+registerPlugin(SavePlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/status-bar/lib/plugin.js
@@ -0,0 +1,105 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("projecteditor/helpers/promise");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+
+/**
+ * Print information about the currently opened file
+ * and the state of the current editor
+ */
+var StatusBarPlugin = Class({
+  extends: Plugin,
+
+  init: function() {
+    this.box = this.host.createElement("hbox", {
+      parent: "#projecteditor-toolbar-bottom"
+    });
+
+    this.activeMode = this.host.createElement("label", {
+      parent: this.box,
+      class: "projecteditor-basic-display"
+    });
+
+    this.cursorPosition = this.host.createElement("label", {
+      parent: this.box,
+      class: "projecteditor-basic-display"
+    });
+
+    this.fileLabel = this.host.createElement("label", {
+      parent: "#plugin-toolbar-left",
+      class: "projecteditor-file-label"
+    });
+  },
+
+  destroy: function() {
+  },
+
+  /**
+   * Print information about the current state of the editor
+   *
+   * @param Editor editor
+   */
+  render: function(editor, resource) {
+    if (!resource || resource.isDir) {
+      this.fileLabel.textContent = "";
+      this.cursorPosition.value = "";
+      return;
+    }
+
+    this.fileLabel.textContent = resource.basename;
+    this.activeMode.value = editor.toString();
+    if (editor.editor) {
+      let cursorStart = editor.editor.getCursor("start");
+      let cursorEnd = editor.editor.getCursor("end");
+      if (cursorStart.line === cursorEnd.line && cursorStart.ch === cursorEnd.ch) {
+        this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch;
+      } else {
+        this.cursorPosition.value = cursorStart.line + " " + cursorStart.ch + " | " +
+                                    cursorEnd.line + " " + cursorEnd.ch;
+      }
+    } else {
+      this.cursorPosition.value = "";
+    }
+  },
+
+
+  /**
+   * Print the current file name
+   *
+   * @param Resource resource
+   */
+  onTreeSelected: function(resource) {
+    if (!resource || resource.isDir) {
+      this.fileLabel.textContent = "";
+      return;
+    }
+    this.fileLabel.textContent = resource.basename;
+  },
+
+  onEditorDeactivated: function(editor) {
+    this.fileLabel.textContent = "";
+    this.cursorPosition.value = "";
+  },
+
+  onEditorChange: function(editor, resource) {
+    this.render(editor, resource);
+  },
+
+  onEditorCursorActivity: function(editor, resource) {
+    this.render(editor, resource);
+  },
+
+  onEditorActivated: function(editor, resource) {
+    this.render(editor, resource);
+  },
+
+});
+
+exports.StatusBarPlugin = StatusBarPlugin;
+registerPlugin(StatusBarPlugin);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/project.js
@@ -0,0 +1,239 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const { scope, on, forget } = require("projecteditor/helpers/event");
+const prefs = require("sdk/preferences/service");
+const { LocalStore } = require("projecteditor/stores/local");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+const promise = require("projecteditor/helpers/promise");
+const { TextEncoder, TextDecoder } = require('sdk/io/buffer');
+const url = require('sdk/url');
+
+const gDecoder = new TextDecoder();
+const gEncoder = new TextEncoder();
+
+/**
+ * A Project keeps track of the opened folders using LocalStore
+ * objects.  Resources are generally requested from the project,
+ * even though the Store is actually keeping track of them.
+ */
+var Project = Class({
+  extends: EventTarget,
+
+  /**
+   * Intialize the Project.
+   *
+   * @param Object options
+   *               Options to be passed into Project.load function
+   */
+  initialize: function(options) {
+    this.localStores = new Map();
+
+    this.load(options);
+  },
+
+  destroy: function() {
+    // We are removing the store because the project never gets persisted.
+    // There may need to be separate destroy functionality that doesn't remove
+    // from project if this is saved to DB.
+    this.removeAllStores();
+  },
+
+  toString: function() {
+    return "[Project] " + this.name;
+  },
+
+  /**
+   * Load a project given metadata about it.
+   *
+   * @param Object options
+   *               Information about the project, containing:
+   *                id: An ID (currently unused, but could be used for saving)
+   *                name: The display name of the project
+   *                directories: An array of path strings to load
+   */
+  load: function(options) {
+    this.id = options.id;
+    this.name = options.name || "Untitled";
+
+    let paths = new Set(options.directories.map(name => OS.Path.normalize(name)));
+
+    for (let [path, store] of this.localStores) {
+      if (!paths.has(path)) {
+        this.removePath(path);
+      }
+    }
+
+    for (let path of paths) {
+      this.addPath(path);
+    }
+  },
+
+  /**
+   * Refresh all project stores from disk
+   *
+   * @returns Promise
+   *          A promise that resolves when everything has been refreshed.
+   */
+  refresh: function() {
+    return Task.spawn(function*() {
+      for (let [path, store] of this.localStores) {
+        yield store.refresh();
+      }
+    }.bind(this));
+  },
+
+
+  /**
+   * Fetch a resource from the backing storage system for the store.
+   *
+   * @param string path
+   *               The path to fetch
+   * @param Object options
+   *               "create": bool indicating whether to create a file if it does not exist.
+   * @returns Promise
+   *          A promise that resolves with the Resource.
+   */
+  resourceFor: function(path, options) {
+    let store = this.storeContaining(path);
+    return store.resourceFor(path, options);
+  },
+
+  /**
+   * Get every resource used inside of the project.
+   *
+   * @returns Array<Resource>
+   *          A list of all Resources in all Stores.
+   */
+  allResources: function() {
+    let resources = [];
+    for (let store of this.allStores()) {
+      resources = resources.concat(store.allResources());
+    }
+    return resources;
+  },
+
+  /**
+   * Get every Path used inside of the project.
+   *
+   * @returns generator-iterator<Store>
+   *          A list of all Stores
+   */
+  allStores: function*() {
+    for (let [path, store] of this.localStores) {
+      yield store;
+    }
+  },
+
+  /**
+   * Get every file path used inside of the project.
+   *
+   * @returns generator-iterator<string>
+   *          A list of all file paths
+   */
+  allPaths: function*() {
+    for (let [path, store] of this.localStores) {
+      yield path;
+    }
+  },
+
+  /**
+   * Get the store that contains a path.
+   *
+   * @returns Store
+   *          The store, if any.  Will return null if no store
+   *          contains the given path.
+   */
+  storeContaining: function(path) {
+    let containingStore = null;
+    for (let store of this.allStores()) {
+      if (store.contains(path)) {
+        // With nested projects, the final containing store will be returned.
+        containingStore = store;
+      }
+    }
+    return containingStore;
+  },
+
+  /**
+   * Add a store at the current path.  If a store already exists
+   * for this path, then return it.
+   *
+   * @param string path
+   * @returns LocalStore
+   */
+  addPath: function(path) {
+    if (!this.localStores.has(path)) {
+      this.addLocalStore(new LocalStore(path));
+    }
+    return this.localStores.get(path);
+  },
+
+  /**
+   * Remove a store for a given path.
+   *
+   * @param string path
+   */
+  removePath: function(path) {
+    this.removeLocalStore(this.localStores.get(path));
+  },
+
+
+  /**
+   * Add the given Store to the project.
+   * Fires a 'store-added' event on the project.
+   *
+   * @param Store store
+   */
+  addLocalStore: function(store) {
+    store.canPair = true;
+    this.localStores.set(store.path, store);
+
+    // Originally StoreCollection.addStore
+    on(this, store, "resource-added", (resource) => {
+      emit(this, "resource-added", resource);
+    });
+    on(this, store, "resource-removed", (resource) => {
+      emit(this, "resource-removed", resource);
+    })
+
+    emit(this, "store-added", store);
+  },
+
+
+  /**
+   * Remove all of the Stores belonging to the project.
+   */
+  removeAllStores: function() {
+    for (let store of this.allStores()) {
+      this.removeLocalStore(store);
+    }
+  },
+
+  /**
+   * Remove the given Store from the project.
+   * Fires a 'store-removed' event on the project.
+   *
+   * @param Store store
+   */
+  removeLocalStore: function(store) {
+    // XXX: tree selection should be reset if active element is affected by
+    // the store being removed
+    if (store) {
+      this.localStores.delete(store.path);
+      forget(this, store);
+      emit(this, "store-removed", store);
+      store.destroy();
+    }
+  }
+});
+
+exports.Project = Project;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -0,0 +1,594 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cc, Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { Project } = require("projecteditor/project");
+const { ProjectTreeView } = require("projecteditor/tree");
+const { ShellDeck } = require("projecteditor/shells");
+const { Resource } = require("projecteditor/stores/resource");
+const { registeredPlugins } = require("projecteditor/plugins/core");
+const { EventTarget } = require("sdk/event/target");
+const { on, forget } = require("projecteditor/helpers/event");
+const { emit } = require("sdk/event/core");
+const { merge } = require("sdk/util/object");
+const promise = require("projecteditor/helpers/promise");
+const { ViewHelpers } = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+const { DOMHelpers } = Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
+
+// Enabled Plugins
+require("projecteditor/plugins/dirty/lib/dirty");
+require("projecteditor/plugins/delete/lib/delete");
+require("projecteditor/plugins/new/lib/new");
+require("projecteditor/plugins/save/lib/save");
+require("projecteditor/plugins/image-view/lib/plugin");
+require("projecteditor/plugins/app-manager/lib/plugin");
+require("projecteditor/plugins/status-bar/lib/plugin");
+
+// Uncomment to enable logging.
+// require("projecteditor/plugins/logging/lib/logging");
+
+/**
+ * This is the main class tying together an instance of the ProjectEditor.
+ * The frontend is contained inside of this.iframe, which loads projecteditor.xul.
+ *
+ * Usage:
+ *   let projecteditor = new ProjectEditor(frame);
+ *   projecteditor.loaded.then((projecteditor) => {
+ *      // Ready to use.
+ *   });
+ *
+ * Responsible for maintaining:
+ *   - The list of Plugins for this instance.
+ *   - The ShellDeck, which includes all Shells for opened Resources
+ *   -- Shells take in a Resource, and construct the appropriate Editor
+ *   - The Project, which includes all Stores for this instance
+ *   -- Stores manage all Resources starting from a root directory
+ *   --- Resources are a representation of a file on disk
+ *   - The ProjectTreeView that builds the UI for interacting with the
+ *     project.
+ *
+ * This object emits the following events:
+ *   - "onEditorDestroyed": When editor is destroyed
+ *   - "onEditorSave": When editor is saved
+ *   - "onEditorLoad": When editor is loaded
+ *   - "onEditorActivated": When editor is activated
+ *   - "onEditorChange": When editor is changed
+ *   - "onEditorCursorActivity": When there is cursor activity in a text editor
+ *   - "onCommand": When a command happens
+ *   - "onEditorDestroyed": When editor is destroyed
+ *
+ * The events can be bound like so:
+ *   projecteditor.on("onEditorCreated", (editor) => { });
+ */
+var ProjectEditor = Class({
+  extends: EventTarget,
+
+  /**
+   * Initialize ProjectEditor, and load into an iframe if specified.
+   *
+   * @param Iframe iframe
+   *        The iframe to inject the DOM into.  If this is not
+   *        specified, then this.load(frame) will need to be called
+   *        before accessing ProjectEditor.
+   */
+  initialize: function(iframe) {
+    this._onTreeSelected = this._onTreeSelected.bind(this);
+    this._onEditorCreated = this._onEditorCreated.bind(this);
+    this._onEditorActivated = this._onEditorActivated.bind(this);
+    this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
+    this._updateEditorMenuItems = this._updateEditorMenuItems.bind(this);
+
+    if (iframe) {
+      this.load(iframe);
+    }
+  },
+
+  /**
+   * Load the instance inside of a specified iframe.
+   * This can be called more than once, and it will return the promise
+   * from the first call.
+   *
+   * @param Iframe iframe
+   *        The iframe to inject the projecteditor DOM into
+   * @returns Promise
+   *          A promise that is resolved once the iframe has been
+   *          loaded.
+   */
+  load: function(iframe) {
+    if (this.loaded) {
+      return this.loaded;
+    }
+
+    let deferred = promise.defer();
+    this.loaded = deferred.promise;
+    this.iframe = iframe;
+
+    let domReady = () => {
+      this._onLoad();
+      deferred.resolve(this);
+    };
+
+    let domHelper = new DOMHelpers(this.iframe.contentWindow);
+    domHelper.onceDOMReady(domReady);
+
+    this.iframe.setAttribute("src", ITCHPAD_URL);
+
+    return this.loaded;
+  },
+
+  /**
+   * Build the projecteditor DOM inside of this.iframe.
+   */
+  _onLoad: function() {
+    this.document = this.iframe.contentDocument;
+    this.window = this.iframe.contentWindow;
+
+    this._buildSidebar();
+
+    this.window.addEventListener("unload", this.destroy.bind(this));
+
+    // Editor management
+    this.shells = new ShellDeck(this, this.document);
+    this.shells.on("editor-created", this._onEditorCreated);
+    this.shells.on("editor-activated", this._onEditorActivated);
+    this.shells.on("editor-deactivated", this._onEditorDeactivated);
+
+    let shellContainer = this.document.querySelector("#shells-deck-container");
+    shellContainer.appendChild(this.shells.elt);
+
+    let popup = this.document.querySelector("#edit-menu-popup");
+    popup.addEventListener("popupshowing", this.updateEditorMenuItems);
+
+    // We are not allowing preset projects for now - rebuild a fresh one
+    // each time.
+    this.setProject(new Project({
+      id: "",
+      name: "",
+      directories: [],
+      openFiles: []
+    }));
+
+    this._initCommands();
+    this._initPlugins();
+  },
+
+
+  /**
+   * Create the project tree sidebar that lists files.
+   */
+  _buildSidebar: function() {
+    this.projectTree = new ProjectTreeView(this.document, {
+      resourceVisible: this.resourceVisible.bind(this),
+      resourceFormatter: this.resourceFormatter.bind(this)
+    });
+    this.projectTree.on("selection", this._onTreeSelected);
+
+    let sourcesBox = this.document.querySelector("#sources");
+    sourcesBox.appendChild(this.projectTree.elt);
+  },
+
+  /**
+   * Set up listeners for commands to dispatch to all of the plugins
+   */
+  _initCommands: function() {
+    this.commands = this.document.querySelector("#projecteditor-commandset");
+    this.commands.addEventListener("command", (evt) => {
+      evt.stopPropagation();
+      evt.preventDefault();
+      this.pluginDispatch("onCommand", evt.target.id, evt.target);
+    });
+  },
+
+  /**
+   * Initialize each plugin in registeredPlugins
+   */
+  _initPlugins: function() {
+    this._plugins = [];
+
+    for (let plugin of registeredPlugins) {
+      try {
+        this._plugins.push(plugin(this));
+      } catch(ex) {
+        console.exception(ex);
+      }
+    }
+
+    this.pluginDispatch("lateInit");
+  },
+
+  /**
+   * Enable / disable necessary menu items using globalOverlay.js.
+   */
+  _updateEditorMenuItems: function() {
+    this.window.goUpdateGlobalEditMenuItems();
+    this.window.goUpdateGlobalEditMenuItems();
+    let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
+    commands.forEach(this.window.goUpdateCommand);
+  },
+
+  /**
+   * Destroy all objects on the iframe unload event.
+   */
+  destroy: function() {
+    this._plugins.forEach(plugin => { plugin.destroy(); });
+
+    this.project.allResources().forEach((resource) => {
+      let editor = this.editorFor(resource);
+      if (editor) {
+        editor.destroy();
+      }
+    });
+
+    forget(this, this.project);
+    this.project.destroy();
+    this.project = null;
+    this.projectTree.destroy();
+    this.projectTree = null;
+  },
+
+  /**
+   * Set the current project viewed by the projecteditor.
+   *
+   * @param Project project
+   *        The project to set.
+   */
+  setProject: function(project) {
+    if (this.project) {
+      forget(this, this.project);
+    }
+    this.project = project;
+    this.projectTree.setProject(project);
+
+    // Whenever a store gets removed, clean up any editors that
+    // exist for resources within it.
+    on(this, project, "store-removed", (store) => {
+      store.allResources().forEach((resource) => {
+        let editor = this.editorFor(resource);
+        if (editor) {
+          editor.destroy();
+        }
+      });
+    });
+  },
+
+  /**
+   * Set the current project viewed by the projecteditor to a single path,
+   * used by the app manager.
+   *
+   * @param string path
+   *               The file path to set
+   * @param Object opts
+   *               Custom options used by the project. See plugins/app-manager.
+   * @param Promise
+   *        Promise that is resolved once the project is ready to be used.
+   */
+  setProjectToAppPath: function(path, opts = {}) {
+    this.project.appManagerOpts = opts;
+    this.project.removeAllStores();
+    this.project.addPath(path);
+    return this.project.refresh();
+  },
+
+  /**
+   * Open a resource in a particular shell.
+   *
+   * @param Resource resource
+   *                 The file to be opened.
+   */
+  openResource: function(resource) {
+    this.shells.open(resource);
+    this.projectTree.selectResource(resource);
+  },
+
+  /**
+   * When a node is selected in the tree, open its associated editor.
+   *
+   * @param Resource resource
+   *                 The file that has been selected
+   */
+  _onTreeSelected: function(resource) {
+    // Don't attempt to open a directory that is not the root element.
+    if (resource.isDir && resource.parent) {
+      return;
+    }
+    this.pluginDispatch("onTreeSelected", resource);
+    this.openResource(resource);
+  },
+
+  /**
+   * Create an xul element with options
+   *
+   * @param string type
+   *               The tag name of the element to create.
+   * @param Object options
+   *               "command": DOMNode or string ID of a command element.
+   *               "parent": DOMNode or selector of parent to append child to.
+   *               anything other keys are set as an attribute as the element.
+   * @returns DOMElement
+   *          The element that has been created.
+   */
+  createElement: function(type, options) {
+    let elt = this.document.createElement(type);
+
+    let parent;
+
+    for (let opt in options) {
+      if (opt === "command") {
+        let command = typeof(options.command) === "string" ? options.command : options.command.id;
+        elt.setAttribute("command", command);
+      } else if (opt === "parent") {
+        continue;
+      } else {
+        elt.setAttribute(opt, options[opt]);
+      }
+    }
+
+    if (options.parent) {
+      let parent = options.parent;
+      if (typeof(parent) === "string") {
+        parent = this.document.querySelector(parent);
+      }
+      parent.appendChild(elt);
+    }
+
+    return elt;
+  },
+
+  /**
+   * Create a "menuitem" xul element with options
+   *
+   * @param Object options
+   *               See createElement for available options.
+   * @returns DOMElement
+   *          The menuitem that has been created.
+   */
+  createMenuItem: function(options) {
+    return this.createElement("menuitem", options);
+  },
+
+  /**
+   * Add a command to the projecteditor document.
+   * This method is meant to be used with plugins.
+   *
+   * @param Object definition
+   *               key: a key/keycode string. Example: "f".
+   *               id: Unique ID.  Example: "find".
+   *               modifiers: Key modifiers. Example: "accel".
+   * @returns DOMElement
+   *          The command element that has been created.
+   */
+  addCommand: function(definition) {
+    let command = this.document.createElement("command");
+    command.setAttribute("id", definition.id);
+    if (definition.key) {
+      let key = this.document.createElement("key");
+      key.id = "key_" + definition.id;
+
+      let keyName = definition.key;
+      if (keyName.startsWith("VK_")) {
+        key.setAttribute("keycode", keyName);
+      } else {
+        key.setAttribute("key", keyName);
+      }
+      key.setAttribute("modifiers", definition.modifiers);
+      key.setAttribute("command", definition.id);
+      this.document.getElementById("projecteditor-keyset").appendChild(key);
+    }
+    command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
+    this.document.getElementById("projecteditor-commandset").appendChild(command);
+    return command;
+  },
+
+  /**
+   * Get the instance of a plugin registered with a certain type.
+   *
+   * @param Type pluginType
+   *             The type, such as SavePlugin
+   * @returns Plugin
+   *          The plugin instance matching the specified type.
+   */
+  getPlugin: function(pluginType) {
+    for (let plugin of this.plugins) {
+      if (plugin.constructor === pluginType) {
+        return plugin;
+      }
+    }
+    return null;
+  },
+
+  /**
+   * Get all plugin instances active for the current project
+   *
+   * @returns [Plugin]
+   */
+  get plugins() {
+    if (!this._plugins) {
+      console.log("plugins requested before _plugins was set");
+      return [];
+    }
+    // Could filter further based on the type of project selected,
+    // but no need right now.
+    return this._plugins;
+  },
+
+  /**
+   * Dispatch an onEditorCreated event, and listen for other events specific
+   * to this editor instance.
+   *
+   * @param Editor editor
+   *               The new editor instance.
+   */
+  _onEditorCreated: function(editor) {
+    this.pluginDispatch("onEditorCreated", editor);
+    this._editorListenAndDispatch(editor, "change", "onEditorChange");
+    this._editorListenAndDispatch(editor, "cursorActivity", "onEditorCursorActivity");
+    this._editorListenAndDispatch(editor, "load", "onEditorLoad");
+    this._editorListenAndDispatch(editor, "save", "onEditorSave");
+  },
+
+  /**
+   * Dispatch an onEditorActivated event and finish setting up once the
+   * editor is ready to use.
+   *
+   * @param Editor editor
+   *               The editor instance, which is now appended in the document.
+   * @param Resource resource
+   *               The resource used by the editor
+   */
+  _onEditorActivated: function(editor, resource) {
+    editor.setToolbarVisibility();
+    this.pluginDispatch("onEditorActivated", editor, resource);
+  },
+
+  /**
+   * Dispatch an onEditorDactivated event once an editor loses focus
+   *
+   * @param Editor editor
+   *               The editor instance, which is no longer active.
+   * @param Resource resource
+   *               The resource used by the editor
+   */
+  _onEditorDeactivated: function(editor, resource) {
+    this.pluginDispatch("onEditorDeactivated", editor, resource);
+  },
+
+  /**
+   * Call a method on all plugins that implement the method.
+   * Also emits the same handler name on `this`.
+   *
+   * @param string handler
+   *               Which function name to call on plugins.
+   * @param ...args args
+   *                All remaining parameters are passed into the handler.
+   */
+  pluginDispatch: function(handler, ...args) {
+    // XXX: Memory leak when console.log an Editor here
+    // console.log("DISPATCHING EVENT TO PLUGIN", handler, args);
+    emit(this, handler, ...args);
+    this.plugins.forEach(plugin => {
+      try {
+        if (handler in plugin) plugin[handler](...args);
+      } catch(ex) {
+        console.error(ex);
+      }
+    })
+  },
+
+  /**
+   * Listen to an event on the editor object and dispatch it
+   * to all plugins that implement the associated method
+   *
+   * @param Editor editor
+   *               Which editor to listen to
+   * @param string event
+   *               Which editor event to listen for
+   * @param string handler
+   *               Which plugin method to call
+   */
+  _editorListenAndDispatch: function(editor, event, handler) {
+    /// XXX: Uncommenting this line also causes memory leak.
+    // console.log("Binding listen and dispatch", editor);
+    editor.on(event, (...args) => {
+      this.pluginDispatch(handler, editor, this.resourceFor(editor), ...args);
+    });
+  },
+
+  /**
+   * Find a shell for a resource.
+   *
+   * @param Resource resource
+   *                 The file to be opened.
+   * @returns Shell
+   */
+  shellFor: function(resource) {
+    return this.shells.shellFor(resource);
+  },
+
+  /**
+   * Returns the Editor for a given resource.
+   *
+   * @param Resource resource
+   *                 The file to check.
+   * @returns Editor
+   *          Instance of the editor for this file.
+   */
+  editorFor: function(resource) {
+    let shell = this.shellFor(resource);
+    return shell ? shell.editor : shell;
+  },
+
+  /**
+   * Returns a resource for the given editor
+   *
+   * @param Editor editor
+   *               The editor to check
+   * @returns Resource
+   *          The resource associated with this editor
+   */
+  resourceFor: function(editor) {
+    if (editor && editor.shell && editor.shell.resource) {
+      return editor.shell.resource;
+    }
+    return null;
+  },
+
+  /**
+   * Decide whether a given resource should be hidden in the tree.
+   *
+   * @param Resource resource
+   *                 The resource in the tree
+   * @returns Boolean
+   *          True if the node should be visible, false if hidden.
+   */
+  resourceVisible: function(resource) {
+    return true;
+  },
+
+  /**
+   * Format the given node for display in the resource tree view.
+   *
+   * @param Resource resource
+   *                 The file to be opened.
+   * @param DOMNode elt
+   *                The element in the tree to render into.
+   */
+  resourceFormatter: function(resource, elt) {
+    let editor = this.editorFor(resource);
+    let renderedByPlugin = false;
+
+    // Allow plugins to override default templating of resource in tree.
+    this.plugins.forEach(plugin => {
+      if (!plugin.onAnnotate) {
+        return;
+      }
+      if (plugin.onAnnotate(resource, editor, elt)) {
+        renderedByPlugin = true;
+      }
+    });
+
+    // If no plugin wants to handle it, just use a string from the resource.
+    if (!renderedByPlugin) {
+      elt.textContent = resource.displayName;
+    }
+  },
+
+  get sourcesVisible() {
+    return this.sourceToggle.hasAttribute("pane-collapsed");
+  },
+
+  get currentShell() {
+    return this.shells.currentShell;
+  },
+
+  get currentEditor() {
+    return this.shells.currentEditor;
+  },
+});
+
+exports.ProjectEditor = ProjectEditor;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/shells.js
@@ -0,0 +1,210 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const { EditorTypeForResource } = require("projecteditor/editors");
+const NetworkHelper = require("devtools/toolkit/webconsole/network-helper");
+
+/**
+ * The Shell is the object that manages the editor for a single resource.
+ * It is in charge of selecting the proper Editor (text/image/plugin-defined)
+ * and instantiating / appending the editor.
+ * This object is not exported, it is just used internally by the ShellDeck.
+ *
+ * This object has a promise `editorAppended`, that will resolve once the editor
+ * is ready to be used.
+ */
+var Shell = Class({
+  extends: EventTarget,
+
+  /**
+   * @param ProjectEditor host
+   * @param Resource resource
+   */
+  initialize: function(host, resource) {
+    this.host = host;
+    this.doc = host.document;
+    this.resource = resource;
+    this.elt = this.doc.createElement("vbox");
+    this.elt.shell = this;
+
+    let constructor = this._editorTypeForResource();
+
+    this.editor = constructor(this.doc, this.host);
+    this.editor.shell = this;
+    this.editorAppended = this.editor.appended;
+
+    let loadDefer = promise.defer();
+    this.editor.on("load", () => {
+      loadDefer.resolve();
+    });
+
+    this.editorLoaded = loadDefer.promise;
+
+    this.elt.appendChild(this.editor.elt);
+  },
+
+  /**
+   * Start loading the resource.  The 'load' event happens as
+   * a result of this function, so any listeners to 'editorAppended'
+   * need to be added before calling this.
+   */
+  load: function() {
+    this.editor.load(this.resource);
+  },
+
+  /**
+   * Make sure the correct editor is selected for the resource.
+   * @returns Type:Editor
+   */
+  _editorTypeForResource: function() {
+    let resource = this.resource;
+    let constructor = EditorTypeForResource(resource);
+
+    if (this.host.plugins) {
+      this.host.plugins.forEach(plugin => {
+        if (plugin.editorForResource) {
+          let pluginEditor = plugin.editorForResource(resource);
+          if (pluginEditor) {
+            constructor = pluginEditor;
+          }
+        }
+      });
+    }
+
+    return constructor;
+  }
+});
+
+/**
+ * The ShellDeck is in charge of managing the list of active Shells for
+ * the current ProjectEditor instance (aka host).
+ *
+ * This object emits the following events:
+ *   - "editor-created": When an editor is initially created
+ *   - "editor-activated": When an editor is ready to use
+ *   - "editor-deactivated": When an editor is ready to use
+ */
+var ShellDeck = Class({
+  extends: EventTarget,
+
+  /**
+   * @param ProjectEditor host
+   * @param Document document
+   */
+  initialize: function(host, document) {
+    this.doc = document;
+    this.host = host;
+    this.deck = this.doc.createElement("deck");
+    this.deck.setAttribute("flex", "1");
+    this.elt = this.deck;
+
+    this.shells = new Map();
+
+    this._activeShell = null;
+  },
+
+  /**
+   * Open a resource in a Shell.  Will create the Shell
+   * if it doesn't exist yet.
+   *
+   * @param Resource resource
+   *                 The file to be opened
+   * @returns Shell
+   */
+  open: function(defaultResource) {
+    let shell = this.shellFor(defaultResource);
+    if (!shell) {
+      shell = this._createShell(defaultResource);
+      this.shells.set(defaultResource, shell);
+    }
+    this.selectShell(shell);
+    return shell;
+  },
+
+  /**
+   * Create a new Shell for a resource.  Called by `open`.
+   *
+   * @returns Shell
+   */
+  _createShell: function(defaultResource) {
+    let shell = Shell(this.host, defaultResource);
+
+    shell.editorAppended.then(() => {
+      this.shells.set(shell.resource, shell);
+      emit(this, "editor-created", shell.editor);
+      if (this.currentShell === shell) {
+        this.selectShell(shell);
+      }
+
+    });
+
+    shell.load();
+    this.deck.appendChild(shell.elt);
+    return shell;
+  },
+
+  /**
+   * Select a given shell and open its editor.
+   * Will fire editor-deactivated on the old selected Shell (if any),
+   * and editor-activated on the new one once it is ready
+   *
+   * @param Shell shell
+   */
+  selectShell: function(shell) {
+    // Don't fire another activate if this is already the active shell
+    if (this._activeShell != shell) {
+      if (this._activeShell) {
+        emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
+      }
+      this.deck.selectedPanel = shell.elt;
+      this._activeShell = shell;
+      shell.editorLoaded.then(() => {
+        // Handle case where another shell has been requested before this
+        // one is finished loading.
+        if (this._activeShell === shell) {
+          emit(this, "editor-activated", shell.editor, shell.resource);
+        }
+      });
+    }
+  },
+
+  /**
+   * Find a Shell for a Resource.
+   *
+   * @param Resource resource
+   * @returns Shell
+   */
+  shellFor: function(resource) {
+    return this.shells.get(resource);
+  },
+
+  /**
+   * The currently active Shell.  Note: the editor may not yet be available
+   * on the current shell.  Best to wait for the 'editor-activated' event
+   * instead.
+   *
+   * @returns Shell
+   */
+  get currentShell() {
+    return this._activeShell;
+  },
+
+  /**
+   * The currently active Editor, or null if it is not ready.
+   *
+   * @returns Editor
+   */
+  get currentEditor() {
+    let shell = this.currentShell;
+    return shell ? shell.editor : null;
+  },
+
+});
+exports.ShellDeck = ShellDeck;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/stores/base.js
@@ -0,0 +1,58 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cc, Ci, Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("projecteditor/helpers/promise");
+
+/**
+ * A Store object maintains a collection of Resource objects stored in a tree.
+ *
+ * The Store class should not be instantiated directly.  Instead, you should
+ * use a class extending it - right now this is only a LocalStore.
+ *
+ * Events:
+ * This object emits the 'resource-added' and 'resource-removed' events.
+ */
+var Store = Class({
+  extends: EventTarget,
+
+  /**
+   * Should be called during initialize() of a subclass.
+   */
+  initStore: function() {
+    this.resources = new Map();
+  },
+
+  refresh: function() {
+    return promise.resolve();
+  },
+
+  /**
+   * Return a sorted Array of all Resources in the Store
+   */
+  allResources: function() {
+    var resources = [];
+    function addResource(resource) {
+      resources.push(resource);
+      resource.childrenSorted.forEach(addResource);
+    }
+    addResource(this.root);
+    return resources;
+  },
+
+  notifyAdd: function(resource) {
+    emit(this, "resource-added", resource);
+  },
+
+  notifyRemove: function(resource) {
+    emit(this, "resource-removed", resource);
+  }
+});
+
+exports.Store = Store;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/stores/local.js
@@ -0,0 +1,219 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cc, Ci, Cu, ChromeWorker } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { emit } = require("sdk/event/core");
+const { Store } = require("projecteditor/stores/base");
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+const promise = require("projecteditor/helpers/promise");
+const { on, forget } = require("projecteditor/helpers/event");
+const { FileResource } = require("projecteditor/stores/resource");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+
+const CHECK_LINKED_DIRECTORY_DELAY = 5000;
+const SHOULD_LIVE_REFRESH = true;
+// XXX: Ignores should be customizable
+const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/;
+
+/**
+ * A LocalStore object maintains a collection of Resource objects
+ * from the file system.
+ *
+ * This object emits the following events:
+ *   - "resource-added": When a resource is added
+ *   - "resource-removed": When a resource is removed
+ */
+var LocalStore = Class({
+  extends: Store,
+
+  defaultCategory: "js",
+
+  initialize: function(path) {
+    this.initStore();
+    this.window = Services.appShell.hiddenDOMWindow;
+    this.path = OS.Path.normalize(path);
+    this.rootPath = this.path;
+    this.displayName = this.path;
+    this.root = this._forPath(this.path);
+    this.notifyAdd(this.root);
+    this.refreshLoop = this.refreshLoop.bind(this);
+    this.refreshLoop();
+  },
+
+  destroy: function() {
+    if (this.window) {
+      this.window.clearTimeout(this._refreshTimeout);
+    }
+    if (this._refreshDeferred) {
+      this._refreshDeferred.reject("destroy");
+    }
+    if (this.worker) {
+      this.worker.terminate();
+    }
+
+    this._refreshTimeout = null;
+    this._refreshDeferred = null;
+    this.window = null;
+    this.worker = null;
+
+    if (this.root) {
+      forget(this, this.root);
+      this.root.destroy();
+    }
+  },
+
+  toString: function() { return "[LocalStore:" + this.path + "]" },
+
+  /**
+   * Return a FileResource object for the given path.  If a FileInfo
+   * is provided the resource will use it, otherwise the FileResource
+   * might not have full information until the next refresh.
+   *
+   * The following parameters are passed into the FileResource constructor
+   * See resource.js for information about them
+   *
+   * @param String path
+   * @param FileInfo info
+   * @returns Resource
+   */
+  _forPath: function(path, info=null) {
+    if (this.resources.has(path)) {
+      return this.resources.get(path);
+    }
+
+    let resource = FileResource(this, path, info);
+    this.resources.set(path, resource);
+    return resource;
+  },
+
+  /**
+   * Return a promise that resolves to a fully-functional FileResource
+   * within this project.  This will hit the disk for stat info.
+   * options:
+   *
+   *   create: If true, a resource will be created even if the underlying
+   *     file doesn't exist.
+   */
+  resourceFor: function(path, options) {
+    path = OS.Path.normalize(path);
+
+    if (this.resources.has(path)) {
+      return promise.resolve(this.resources.get(path));
+    }
+
+    if (!this.contains(path)) {
+      return promise.reject(new Error(path + " does not belong to " + this.path));
+    }
+
+    return Task.spawn(function() {
+      let parent = yield this.resourceFor(OS.Path.dirname(path));
+
+      let info;
+      try {
+        info = yield OS.File.stat(path);
+      } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+        if (!options.create) {
+          throw ex;
+        }
+      }
+
+      let resource = this._forPath(path, info);
+      parent.addChild(resource);
+      throw new Task.Result(resource);
+    }.bind(this));
+  },
+
+  refreshLoop: function() {
+    // XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
+    this.refresh().then(() => {
+      if (SHOULD_LIVE_REFRESH) {
+        this._refreshTimeout = this.window.setTimeout(this.refreshLoop,
+          CHECK_LINKED_DIRECTORY_DELAY);
+      }
+    });
+  },
+
+  _refreshTimeout: null,
+  _refreshDeferred: null,
+
+  /**
+   * Refresh the directory structure.
+   */
+  refresh: function(path=this.rootPath) {
+    if (this._refreshDeferred) {
+      return this._refreshDeferred.promise;
+    }
+    this._refreshDeferred = promise.defer();
+
+    let worker = this.worker = new ChromeWorker("chrome://browser/content/devtools/readdir.js");
+    let start = Date.now();
+
+    worker.onmessage = evt => {
+      // console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt);
+      for (path in evt.data) {
+        let info = evt.data[path];
+        info.path = path;
+
+        let resource = this._forPath(path, info);
+        resource.info = info;
+        if (info.isDir) {
+          let newChildren = new Set();
+          for (let childPath of info.children) {
+            childInfo = evt.data[childPath];
+            newChildren.add(this._forPath(childPath, childInfo));
+          }
+          resource.setChildren(newChildren);
+        }
+        resource.info.children = null;
+      }
+
+      worker = null;
+      this._refreshDeferred.resolve();
+      this._refreshDeferred = null;
+    };
+    worker.onerror = ex => {
+      console.error(ex);
+      worker = null;
+      this._refreshDeferred.reject(ex);
+      this._refreshDeferred = null;
+    }
+    worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX });
+    return this._refreshDeferred.promise;
+  },
+
+  /**
+   * Returns true if the given path would be a child of the store's
+   * root directory.
+   */
+  contains: function(path) {
+    path = OS.Path.normalize(path);
+    let thisPath = OS.Path.split(this.rootPath);
+    let thatPath = OS.Path.split(path)
+
+    if (!(thisPath.absolute && thatPath.absolute)) {
+      throw new Error("Contains only works with absolute paths.");
+    }
+
+    if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) {
+      return false;
+    }
+
+    if (thatPath.components.length <= thisPath.components.length) {
+      return false;
+    }
+
+    for (let i = 0; i < thisPath.components.length; i++) {
+      if (thisPath.components[i] != thatPath.components[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+});
+exports.LocalStore = LocalStore;
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/stores/resource.js
@@ -0,0 +1,340 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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/. */
+
+"use strict";
+
+const { Cc, Ci, Cu } = require("chrome");
+const { TextEncoder, TextDecoder } = require('sdk/io/buffer');
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const URL = require("sdk/url");
+const promise = require("projecteditor/helpers/promise");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+
+const gDecoder = new TextDecoder();
+const gEncoder = new TextEncoder();
+
+/**
+ * A Resource is a single file-like object that can be respresented
+ * as a file for ProjectEditor.
+ *
+ * The Resource class is not exported, and should not be instantiated
+ * Instead, you should use the FileResource class that extends it.
+ *
+ * This object emits the following events:
+ *   - "children-changed": When a child has been added or removed.
+ *                         See setChildren.
+ */
+var Resource = Class({
+  extends: EventTarget,
+
+  refresh: function() { return promise.resolve(this) },
+
+  setURI: function(uri) {
+    if (typeof(uri) === "string") {
+      uri = URL.URL(uri);
+    }
+    this.uri = uri;
+  },
+
+  /**
+   * Return the trailing name component of this.uri.
+   */
+  get basename() { return this.uri.path.replace(/\/+$/, '').replace(/\\/g,'/').replace( /.*\//, '' ); },
+
+  /**
+   * Is there more than 1 child Resource?
+   */
+  get hasChildren() { return this.children && this.children.size > 0; },
+
+  /**
+   * Sorted array of children for display
+   */
+  get childrenSorted() {
+    if (!this.hasChildren) {
+      return [];
+    }
+
+    return [...this.children].sort((a, b)=> {
+      // Put directories above files.
+      if (a.isDir !== b.isDir) {
+        return b.isDir;
+      }
+      return a.basename.toLowerCase() > b.basename.toLowerCase();
+    });
+  },
+
+  /**
+   * Set the children set of this Resource, and notify of any
+   * additions / removals that happened in the change.
+   */
+  setChildren: function(newChildren) {
+    let oldChildren = this.children || new Set();
+    let change = false;
+
+    for (let child of oldChildren) {
+      if (!newChildren.has(child)) {
+        change = true;
+        child.parent = null;
+        this.store.notifyRemove(child);
+      }
+    }
+
+    for (let child of newChildren) {
+      if (!oldChildren.has(child)) {
+        change = true;
+        child.parent = this;
+        this.store.notifyAdd(child);
+      }
+    }
+
+    this.children = newChildren;
+    if (change) {
+      emit(this, "children-changed", this);
+    }
+  },
+
+  /**
+   * Add a resource to children set and notify of the change.
+   *
+   * @param Resource resource
+   */
+  addChild: function(resource) {
+    this.children = this.children || new Set();
+
+    resource.parent = this;
+    this.children.add(resource);
+    this.store.notifyAdd(resource);
+    emit(this, "children-changed", this);
+    return resource;
+  },
+
+  /**
+   * Remove a resource to children set and notify of the change.
+   *
+   * @param Resource resource
+   */
+  removeChild: function(resource) {
+    resource.parent = null;
+    this.children.remove(resource);
+    this.store.notifyRemove(resource);
+    emit(this, "children-changed", this);
+    return resource;
+  },
+
+  /**
+   * Return a set with children, children of children, etc -
+   * gathered recursively.
+   *
+   * @returns Set<Resource>
+   */
+  allDescendants: function() {
+    let set = new Set();
+
+    function addChildren(item) {
+      if (!item.children) {
+        return;
+      }
+
+      for (let child of item.children) {
+        set.add(child);
+      }
+    }
+
+    addChildren(this);
+    for (let item of set) {
+      addChildren(item);
+    }
+
+    return set;
+  },
+});
+
+/**
+ * A FileResource is an implementation of Resource for a File System
+ * backing.  This is exported, and should be used instead of Resource.
+ */
+var FileResource = Class({
+  extends: Resource,
+
+  /**
+   * @param Store store
+   * @param String path
+   * @param FileInfo info
+   *        https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info
+   */
+  initialize: function(store, path, info) {
+    this.store = store;
+    this.path = path;
+
+    this.setURI(URL.URL(URL.fromFilename(path)));
+    this._lastReadModification = undefined;
+
+    this.info = info;
+    this.parent = null;
+  },
+
+  toString: function() {
+    return "[FileResource:" + this.path + "]";
+  },
+
+  destroy: function() {
+    if (this._refreshDeferred) {
+      this._refreshDeferred.reject();
+    }
+    this._refreshDeferred = null;
+  },
+
+  /**
+   * Fetch and cache information about this particular file.
+   * https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File_for_the_main_thread#OS.File.stat
+   *
+   * @returns Promise
+   *          Resolves once the File.stat has finished.
+   */
+  refresh: function() {
+    if (this._refreshDeferred) {
+      return this._refreshDeferred.promise;
+    }
+    this._refreshDeferred = promise.defer();
+    OS.File.stat(this.path).then(info => {
+      this.info = info;
+      if (this._refreshDeferred) {
+        this._refreshDeferred.resolve(this);
+        this._refreshDeferred = null;
+      }
+    });
+    return this._refreshDeferred.promise;
+  },
+
+  /**
+   * A string to be used when displaying this Resource in views
+   */
+  get displayName() {
+    return this.basename + (this.isDir ? "/" : "")
+  },
+
+  /**
+   * Is this FileResource a directory?  Rather than checking children
+   * here, we use this.info.  So this could return a false negative
+   * if there was no info passed in on constructor and the first
+   * refresh hasn't yet finished.
+   */
+  get isDir() {
+    if (!this.info) { return false; }
+    return this.info.isDir && !this.info.isSymLink;
+  },
+
+  /**
+   * Read the file as a string asynchronously.
+   *
+   * @returns Promise
+   *          Resolves with the text of the file.
+   */
+  load: function() {
+    return OS.File.read(this.path).then(bytes => {
+      return gDecoder.decode(bytes);
+    });
+  },
+
+  /**
+   * Add a text file as a child of this FileResource.
+   * This instance must be a directory.
+   *
+   * @param string name
+   *               The filename (path will be generated based on this.path).
+   *        string initial
+   *               The content to write to the new file.
+   * @returns Promise
+   *          Resolves with the new FileResource once it has
+   *          been written to disk.
+   *          Rejected if this is not a directory.
+   */
+  createChild: function(name, initial="") {
+    if (!this.isDir) {
+      return promise.reject(new Error("Cannot add child to a regular file"));
+    }
+
+    let newPath = OS.Path.join(this.path, name);
+
+    let buffer = initial ? gEncoder.encode(initial) : "";
+    return OS.File.writeAtomic(newPath, buffer, {
+      noOverwrite: true
+    }).then(() => {
+      return this.store.refresh();
+    }).then(() => {
+      let resource = this.store.resources.get(newPath);
+      if (!resource) {
+        throw new Error("Error creating " + newPath);
+      }
+      return resource;
+    });
+  },
+
+  /**
+   * Write a string to this file.
+   *
+   * @param string content
+   * @returns Promise
+   *          Resolves once it has been written to disk.
+   *          Rejected if there is an error
+   */
+  save: function(content) {
+    let buffer = gEncoder.encode(content);
+    let path = this.path;
+
+    // XXX: writeAtomic was losing permissions after saving on OSX
+    // return OS.File.writeAtomic(this.path, buffer, { tmpPath: this.path + ".tmp" });
+
+    return Task.spawn(function*() {
+        let pfh = yield OS.File.open(path, {truncate: true});
+        yield pfh.write(buffer);
+        yield pfh.close();
+    });
+  },
+
+  /**
+   * Attempts to get the content type from the file.
+   */
+  get contentType() {
+    if (this._contentType) {
+      return this._contentType;
+    }
+    if (this.isDir) {
+      return "x-directory/normal";
+    }
+    try {
+      this._contentType = mimeService.getTypeFromFile(new FileUtils.File(this.path));
+    } catch(ex) {
+      if (ex.name !== "NS_ERROR_NOT_AVAILABLE" &&
+          ex.name !== "NS_ERROR_FAILURE") {
+        console.error(ex, this.path);
+      }
+      this._contentType = null;
+    }
+    return this._contentType;
+  },
+
+  /**
+   * A string used when determining the type of Editor to open for this.
+   * See editors.js -> EditorTypeForResource.
+   */
+  get contentCategory() {
+    const NetworkHelper = require("devtools/toolkit/webconsole/network-helper");
+    let category = NetworkHelper.mimeCategoryMap[this.contentType];
+    // Special treatment for manifest.webapp.
+    if (!category && this.basename === "manifest.webapp") {
+      return "json";
+    }
+    return category || "txt";
+  }
+});
+
+exports.FileResource = FileResource;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -0,0 +1,557 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 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 { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { emit } = require("sdk/event/core");
+const { EventTarget } = require("sdk/event/target");
+const { merge } = require("sdk/util/object");
+const promise = require("projecteditor/helpers/promise");
+const { InplaceEditor } = require("devtools/shared/inplace-editor");
+const { on, forget } = require("projecteditor/helpers/event");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * ResourceContainer is used as the view of a single Resource in
+ * the tree.  It is not exported.
+ */
+var ResourceContainer = Class({
+  /**
+   * @param ProjectTreeView tree
+   * @param Resource resource
+   */
+  initialize: function(tree, resource) {
+    this.tree = tree;
+    this.resource = resource;
+    this.elt = null;
+    this.expander = null;
+    this.children = null;
+
+    let doc = tree.doc;
+
+    this.elt = doc.createElementNS(HTML_NS, "li");
+    this.elt.classList.add("child");
+
+    this.line = doc.createElementNS(HTML_NS, "div");
+    this.line.classList.add("child");
+    this.line.classList.add("side-menu-widget-item");
+    this.line.setAttribute("theme", "dark");
+    this.line.setAttribute("tabindex", "0");
+
+    this.elt.appendChild(this.line);
+
+    this.highlighter = doc.createElementNS(HTML_NS, "span");
+    this.highlighter.classList.add("highlighter");
+    this.line.appendChild(this.highlighter);
+
+    this.expander = doc.createElementNS(HTML_NS, "span");
+    this.expander.className = "arrow expander";
+    this.expander.setAttribute("open", "");
+    this.line.appendChild(this.expander);
+
+    this.icon = doc.createElementNS(HTML_NS, "span");
+    this.line.appendChild(this.icon);
+
+    this.label = doc.createElementNS(HTML_NS, "span");
+    this.label.className = "file-label";
+    this.line.appendChild(this.label);
+
+    this.line.addEventListener("contextmenu", (ev) => {
+      this.select();
+      this.openContextMenu(ev);
+    }, false);
+
+    this.children = doc.createElementNS(HTML_NS, "ul");
+    this.children.classList.add("children");
+
+    this.elt.appendChild(this.children);
+
+    this.line.addEventListener("click", (evt) => {
+      if (!this.selected) {
+        this.select();
+        this.expanded = true;
+        evt.stopPropagation();
+      }
+    }, false);
+    this.expander.addEventListener("click", (evt) => {
+      this.expanded = !this.expanded;
+      this.select();
+      evt.stopPropagation();
+    }, true);
+
+    this.update();
+  },
+
+  destroy: function() {
+    this.elt.remove();
+    this.expander.remove();
+    this.icon.remove();
+    this.highlighter.remove();
+    this.children.remove();
+    this.label.remove();
+    this.elt = this.expander = this.icon = this.highlighter = this.children = this.label = null;
+  },
+
+  /**
+   * Open the context menu when right clicking on the view.
+   * XXX: We could pass this to plugins to allow themselves
+   * to be register/remove items from the context menu if needed.
+   *
+   * @param Event e
+   */
+  openContextMenu: function(ev) {
+    ev.preventDefault();
+    let popup = this.tree.doc.getElementById("directory-menu-popup");
+    popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
+  },
+
+  /**
+   * Update the view based on the current state of the Resource.
+   */
+  update: function() {
+    let visible = this.tree.options.resourceVisible ?
+      this.tree.options.resourceVisible(this.resource) :
+      true;
+
+    this.elt.hidden = !visible;
+
+    this.tree.options.resourceFormatter(this.resource, this.label);
+
+    this.icon.className = "file-icon";
+
+    let contentCategory = this.resource.contentCategory;
+    let baseName = this.resource.basename || "";
+
+    if (!this.resource.parent) {
+      this.icon.classList.add("icon-none");
+    } else if (this.resource.isDir) {
+      this.icon.classList.add("icon-folder");
+    } else if (baseName.endsWith(".manifest") || baseName.endsWith(".webapp")) {
+      this.icon.classList.add("icon-manifest");
+    } else if (contentCategory === "js") {
+      this.icon.classList.add("icon-js");
+    } else if (contentCategory === "css") {
+      this.icon.classList.add("icon-css");
+    } else if (contentCategory === "html") {
+      this.icon.classList.add("icon-html");
+    } else if (contentCategory === "image") {
+      this.icon.classList.add("icon-img");
+    } else {
+      this.icon.classList.add("icon-file");
+    }
+
+    this.expander.style.visibility = this.resource.hasChildren ? "visible" : "hidden";
+
+  },
+
+  /**
+   * Select this view in the ProjectTreeView.
+   */
+  select: function() {
+    this.tree.selectContainer(this);
+  },
+
+  /**
+   * @returns Boolean
+   *          Is this view currently selected
+   */
+  get selected() {
+    return this.line.classList.contains("selected");
+  },
+
+  /**
+   * Set the selected state in the UI.
+   */
+  set selected(v) {
+    if (v) {
+      this.line.classList.add("selected");
+    } else {
+      this.line.classList.remove("selected");
+    }
+  },
+
+  /**
+   * @returns Boolean
+   *          Are any children visible.
+   */
+  get expanded() {
+    return !this.elt.classList.contains("tree-collapsed");
+  },
+
+  /**
+   * Set the visiblity state of children.
+   */
+  set expanded(v) {
+    if (v) {
+      this.elt.classList.remove("tree-collapsed");
+      this.expander.setAttribute("open", "");
+    } else {
+      this.expander.removeAttribute("open");
+      this.elt.classList.add("tree-collapsed");
+    }
+  }
+});
+
+/**
+ * TreeView is a view managing a list of children.
+ * It is not to be instantiated directly - only extended.
+ * Use ProjectTreeView instead.
+ */
+var TreeView = Class({
+  extends: EventTarget,
+
+  /**
+   * @param Document document
+   * @param Object options
+   *               - resourceFormatter: a function(Resource, DOMNode)
+   *                 that renders the resource into the view
+   *               - resourceVisible: a function(Resource) -> Boolean
+   *                 that determines if the resource should show up.
+   */
+  initialize: function(document, options) {
+    this.doc = document;
+    this.options = merge({
+      resourceFormatter: function(resource, elt) {
+        elt.textContent = resource.toString();
+      }
+    }, options);
+    this.models = new Set();
+    this.roots = new Set();
+    this._containers = new Map();
+    this.elt = document.createElement("vbox");
+    this.elt.tree = this;
+    this.elt.className = "side-menu-widget-container sources-tree";
+    this.elt.setAttribute("with-arrows", "true");
+    this.elt.setAttribute("theme", "dark");
+    this.elt.setAttribute("flex", "1");
+
+    this.children = document.createElementNS(HTML_NS, "ul");
+    this.children.setAttribute("flex", "1");
+    this.elt.appendChild(this.children);
+
+    this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
+    this.updateResource = this.updateResource.bind(this);
+  },
+
+  destroy: function() {
+    this._destroyed = true;
+    this.elt.remove();
+  },
+
+  /**
+   * Prompt the user to create a new file in the tree.
+   *
+   * @param string initial
+   *               The suggested starting file name
+   * @param Resource parent
+   * @param Resource sibling
+   *                 Which resource to put this next to.  If not set,
+   *                 it will be put in front of all other children.
+   *
+   * @returns Promise
+   *          Resolves once the prompt has been successful,
+   *          Rejected if it is cancelled
+   */
+  promptNew: function(initial, parent, sibling=null) {
+    let deferred = promise.defer();
+
+    let parentContainer = this._containers.get(parent);
+    let item = this.doc.createElement("li");
+    item.className = "child";
+    let placeholder = this.doc.createElementNS(HTML_NS, "div");
+    placeholder.className = "child";
+    item.appendChild(placeholder);
+
+    let children = parentContainer.children;
+    sibling = sibling ? this._containers.get(sibling).elt : null;
+    parentContainer.children.insertBefore(item, sibling ? sibling.nextSibling : children.firstChild);
+
+    new InplaceEditor({
+      element: placeholder,
+      initial: initial,
+      start: editor => {
+        editor.input.select();
+      },
+      done: function(val, commit) {
+        if (commit) {
+          deferred.resolve(val);
+        } else {
+          deferred.reject(val);
+        }
+        parentContainer.line.focus();
+      },
+      destroy: () => {
+        item.parentNode.removeChild(item);
+      },
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Add a new Store into the TreeView
+   *
+   * @param Store model
+   */
+  addModel: function(model) {
+    if (this.models.has(model)) {
+      // Requesting to add a model that already exists
+      return;
+    }
+    this.models.add(model);
+    let placeholder = this.doc.createElementNS(HTML_NS, "li");
+    placeholder.style.display = "none";
+    this.children.appendChild(placeholder);
+    this.roots.add(model.root);
+    model.root.refresh().then(root => {
+      if (this._destroyed || !this.models.has(model)) {
+        // model may have been removed during the initial refresh.
+        // In this case, do not import the resource or add to DOM, just leave it be.
+        return;
+      }
+      let container = this.importResource(root);
+      container.line.classList.add("side-menu-widget-group-title");
+      container.line.setAttribute("theme", "dark");
+      this.selectContainer(container);
+
+      this.children.insertBefore(container.elt, placeholder);
+      this.children.removeChild(placeholder);
+    });
+  },
+
+  /**
+   * Remove a Store from the TreeView
+   *
+   * @param Store model
+   */
+  removeModel: function(model) {
+    this.models.delete(model);
+    this.removeResource(model.root);
+  },
+
+
+  /**
+   * Get the ResourceContainer.  Used for testing the view.
+   *
+   * @param Resource resource
+   * @returns ResourceContainer
+   */
+  getViewContainer: function(resource) {
+    return this._containers.get(resource);
+  },
+
+  /**
+   * Select a ResourceContainer in the tree.
+   *
+   * @param ResourceContainer container
+   */
+  selectContainer: function(container) {
+    if (this.selectedContainer === container) {
+      return;
+    }
+    if (this.selectedContainer) {
+      this.selectedContainer.selected = false;
+    }
+    this.selectedContainer = container;
+    container.selected = true;
+    emit(this, "selection", container.resource);
+  },
+
+  /**
+   * Select a Resource in the tree.
+   *
+   * @param Resource resource
+   */
+  selectResource: function(resource) {
+    this.selectContainer(this._containers.get(resource));
+  },
+
+  /**
+   * Get the currently selected Resource
+   *
+   * @param Resource resource
+   */
+  getSelectedResource: function() {
+    return this.selectedContainer.resource;
+  },
+
+  /**
+   * Insert a Resource into the view.
+   * Makes a new ResourceContainer if needed
+   *
+   * @param Resource resource
+   */
+  importResource: function(resource) {
+    if (!resource) {
+      return null;
+    }
+
+    if (this._containers.has(resource)) {
+      return this._containers.get(resource);
+    }
+    var container = ResourceContainer(this, resource);
+    this._containers.set(resource, container);
+    this._updateChildren(container);
+
+    on(this, resource, "children-changed", this.resourceChildrenChanged);
+    on(this, resource, "label-change", this.updateResource);
+
+    return container;
+  },
+
+  /**
+   * Delete a Resource from the FileSystem.  XXX: This should
+   * definitely be moved away from here, maybe to the store?
+   *
+   * @param Resource resource
+   */
+  deleteResource: function(resource) {
+    if (resource.isDir) {
+      return OS.File.removeDir(resource.path);
+    } else {
+      return OS.File.remove(resource.path);
+    }
+  },
+
+  /**
+   * Remove a Resource (including children) from the view.
+   *
+   * @param Resource resource
+   */
+  removeResource: function(resource) {
+    let toRemove = resource.allDescendants();
+    toRemove.add(resource);
+    for (let remove of toRemove) {
+      this._removeResource(remove);
+    }
+  },
+
+  /**
+   * Remove an individual Resource (but not children) from the view.
+   *
+   * @param Resource resource
+   */
+  _removeResource: function(resource) {
+    resource.off("children-changed", this.resourceChildrenChanged);
+    resource.off("label-change", this.updateResource);
+    if (this._containers.get(resource)) {
+      this._containers.get(resource).destroy();
+      this._containers.delete(resource);
+    }
+  },
+
+  /**
+   * Listener for when a resource has new children.
+   * This can happen as files are being loaded in from FileSystem, for example.
+   *
+   * @param Resource resource
+   */
+  resourceChildrenChanged: function(resource) {
+    this.updateResource(resource);
+    this._updateChildren(this._containers.get(resource));
+  },
+
+  /**
+   * Listener for when a label in the view has been updated.
+   * For example, the 'dirty' plugin marks changed files with an '*'
+   * next to the filename, and notifies with this event.
+   *
+   * @param Resource resource
+   */
+  updateResource: function(resource) {
+    let container = this._containers.get(resource);
+    container.update();
+  },
+
+  /**
+   * Build necessary ResourceContainers for a Resource and its
+   * children, then append them into the view.
+   *
+   * @param ResourceContainer container
+   */
+  _updateChildren: function(container) {
+    let resource = container.resource;
+    let fragment = this.doc.createDocumentFragment();
+    if (resource.children) {
+      for (let child of resource.childrenSorted) {
+        let childContainer = this.importResource(child);
+        fragment.appendChild(childContainer.elt);
+      }
+    }
+
+    while (container.children.firstChild) {
+      container.children.firstChild.remove();
+    }
+
+    container.children.appendChild(fragment);
+  },
+});
+
+/**
+ * ProjectTreeView is the implementation of TreeView
+ * that is exported.  This is the class that is to be used
+ * directly.
+ */
+var ProjectTreeView = Class({
+  extends: TreeView,
+
+  /**
+   * See TreeView.initialize
+   *
+   * @param Document document
+   * @param Object options
+   */
+  initialize: function(document, options) {
+    TreeView.prototype.initialize.apply(this, arguments);
+  },
+
+  destroy: function() {
+    this.forgetProject();
+    TreeView.prototype.destroy.apply(this, arguments);
+  },
+
+  /**
+   * Remove current project and empty the tree
+   */
+  forgetProject: function() {
+    if (this.project) {
+      forget(this, this.project);
+      for (let store of this.project.allStores()) {
+        this.removeModel(store);
+      }
+    }
+  },
+
+  /**
+   * Show a project in the tree
+   *
+   * @param Project project
+   *        The project to render into a tree
+   */
+  setProject: function(project) {
+    this.forgetProject();
+    this.project = project;
+    if (this.project) {
+      on(this, project, "store-added", this.addModel.bind(this));
+      on(this, project, "store-removed", this.removeModel.bind(this));
+      on(this, project, "project-saved", this.refresh.bind(this));
+      this.refresh();
+    }
+  },
+
+  /**
+   * Refresh the tree with all of the current project stores
+   */
+  refresh: function() {
+    for (let store of this.project.allStores()) {
+      this.addModel(store);
+    }
+  }
+});
+
+exports.ProjectTreeView = ProjectTreeView;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['test']
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+  head.js
+  helper_homepage.html
+
+[browser_projecteditor_delete_file.js]
+[browser_projecteditor_editing_01.js]
+[browser_projecteditor_immediate_destroy.js]
+[browser_projecteditor_init.js]
+[browser_projecteditor_new_file.js]
+[browser_projecteditor_stores.js]
+[browser_projecteditor_tree_selection.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_delete_file.js
@@ -0,0 +1,80 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test tree selection functionality
+
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  ok(true, "ProjectEditor has loaded");
+
+  let root = [...projecteditor.project.allStores()][0].root;
+  is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+  for (let child of root.children) {
+    yield deleteWithContextMenu(projecteditor.projectTree.getViewContainer(child));
+  }
+
+  function onPopupShow(contextMenu) {
+    let defer = promise.defer();
+    contextMenu.addEventListener("popupshown", function onpopupshown() {
+      contextMenu.removeEventListener("popupshown", onpopupshown);
+      defer.resolve();
+    });
+    return defer.promise;
+  }
+
+  function onPopupHide(contextMenu) {
+    let defer = promise.defer();
+    contextMenu.addEventListener("popuphidden", function popuphidden() {
+      contextMenu.removeEventListener("popuphidden", popuphidden);
+      defer.resolve();
+    });
+    return defer.promise;
+  }
+
+  function openContextMenuOn(node) {
+    EventUtils.synthesizeMouseAtCenter(
+      node,
+      {button: 2, type: "contextmenu"},
+      node.ownerDocument.defaultView
+    );
+  }
+
+  function deleteWithContextMenu(container) {
+    let defer = promise.defer();
+
+    let resource = container.resource;
+    let popup = projecteditor.document.getElementById("directory-menu-popup");
+    info ("Going to attempt deletion for: " + resource.path)
+
+    onPopupShow(popup).then(function () {
+      let deleteCommand = popup.querySelector("[command=cmd-delete]");
+      ok (deleteCommand, "Delete command exists in popup");
+      is (deleteCommand.getAttribute("hidden"), "", "Delete command is visible");
+      is (deleteCommand.getAttribute("disabled"), "", "Delete command is enabled");
+
+      onPopupHide(popup).then(() => {
+        ok (true, "Popup has been hidden, waiting for project refresh");
+        projecteditor.project.refresh().then(() => {
+          OS.File.stat(resource.path).then(() => {
+            ok (false, "The file was not deleted");
+            defer.resolve();
+          }, (ex) => {
+            ok (ex instanceof OS.File.Error && ex.becauseNoSuchFile, "OS.File.stat promise was rejected because the file is gone");
+            defer.resolve();
+          });
+        });
+      });
+
+      deleteCommand.click();
+      popup.hidePopup();
+    });
+
+    openContextMenuOn(container.label);
+
+    return defer.promise;
+  }
+
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js
@@ -0,0 +1,94 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test ProjectEditor basic functionality
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  let TEMP_PATH = [...projecteditor.project.allPaths()][0];
+
+  is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+  ok (projecteditor.currentEditor, "There is an editor for projecteditor");
+  let resources = projecteditor.project.allResources();
+
+  resources.forEach((r, i) => {
+    console.log("Resource detected", r.path, i);
+  });
+
+  let stylesCss = resources.filter(r=>r.basename === "styles.css")[0];
+  yield selectFile(projecteditor, stylesCss);
+  yield testEditFile(projecteditor, getTempFile("css/styles.css").path, "body,html { color: orange; }");
+
+  let indexHtml = resources.filter(r=>r.basename === "index.html")[0];
+  yield selectFile(projecteditor, indexHtml);
+  yield testEditFile(projecteditor, getTempFile("index.html").path, "<h1>Changed Content Again</h1>");
+
+  let license = resources.filter(r=>r.basename === "LICENSE")[0];
+  yield selectFile(projecteditor, license);
+  yield testEditFile(projecteditor, getTempFile("LICENSE").path, "My new license");
+
+  let readmeMd = resources.filter(r=>r.basename === "README.md")[0];
+  yield selectFile(projecteditor, readmeMd);
+  yield testEditFile(projecteditor, getTempFile("README.md").path, "My new license");
+
+  let scriptJs = resources.filter(r=>r.basename === "script.js")[0];
+  yield selectFile(projecteditor, scriptJs);
+  yield testEditFile(projecteditor, getTempFile("js/script.js").path, "alert('hi')");
+
+  let vectorSvg = resources.filter(r=>r.basename === "vector.svg")[0];
+  yield selectFile(projecteditor, vectorSvg);
+  yield testEditFile(projecteditor, getTempFile("img/icons/vector.svg").path, "<svg></svg>");
+});
+
+function selectFile (projecteditor, resource) {
+  ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+  projecteditor.projectTree.selectResource(resource);
+
+  if (resource.isDir) {
+    return;
+  }
+
+  let [editorActivated] = yield promise.all([
+    onceEditorActivated(projecteditor)
+  ]);
+
+  is (editorActivated, projecteditor.currentEditor,  "Editor has been activated for " + resource.path);
+}
+
+function testEditFile(projecteditor, filePath, newData) {
+  info ("Testing file editing for: " + filePath);
+
+  let initialData = yield getFileData(filePath);
+  let editor = projecteditor.currentEditor;
+  let resource = projecteditor.resourceFor(editor);
+  let viewContainer= projecteditor.projectTree.getViewContainer(resource);
+  let originalTreeLabel = viewContainer.label.textContent;
+
+  is (resource.path, filePath, "Resource path is set correctly");
+  is (editor.editor.getText(), initialData, "Editor is loaded with correct file contents");
+
+  info ("Setting text in the editor and doing checks before saving");
+
+  editor.editor.setText(newData);
+  is (editor.editor.getText(), newData, "Editor has been filled with new data");
+  is (viewContainer.label.textContent, "*" + originalTreeLabel, "Label is marked as changed");
+
+  info ("Saving the editor and checking to make sure the file gets saved on disk");
+
+  editor.save(resource);
+
+  let savedResource = yield onceEditorSave(projecteditor);
+
+  is (viewContainer.label.textContent, originalTreeLabel, "Label is unmarked as changed");
+  is (savedResource.path, filePath, "The saved resouce path matches the original file path");
+  is (savedResource, resource, "The saved resource is the same as the original resource");
+
+  let savedData = yield getFileData(filePath);
+  is (savedData, newData, "Data has been correctly saved to disk");
+
+  info ("Finished checking saving for " + filePath);
+
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_immediate_destroy.js
@@ -0,0 +1,62 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that projecteditor can be destroyed in various states of loading
+// without causing any leaks or exceptions.
+
+let test = asyncTest(function* () {
+
+  info ("Testing tab closure when projecteditor is in various states");
+
+  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+    let iframe = content.document.getElementById("projecteditor-iframe");
+    ok (iframe, "Tab has placeholder iframe for projecteditor");
+
+    info ("Closing the tab without doing anything");
+    gBrowser.removeCurrentTab();
+  });
+
+  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+    let iframe = content.document.getElementById("projecteditor-iframe");
+    ok (iframe, "Tab has placeholder iframe for projecteditor");
+
+    let projecteditor = ProjectEditor.ProjectEditor();
+    ok (projecteditor, "ProjectEditor has been initialized");
+
+    info ("Closing the tab before attempting to load");
+    gBrowser.removeCurrentTab();
+  });
+
+  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+    let iframe = content.document.getElementById("projecteditor-iframe");
+    ok (iframe, "Tab has placeholder iframe for projecteditor");
+
+    let projecteditor = ProjectEditor.ProjectEditor();
+    ok (projecteditor, "ProjectEditor has been initialized");
+
+    projecteditor.load(iframe);
+
+    info ("Closing the tab after a load is requested, but before load is finished");
+    gBrowser.removeCurrentTab();
+  });
+
+  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+    let iframe = content.document.getElementById("projecteditor-iframe");
+    ok (iframe, "Tab has placeholder iframe for projecteditor");
+
+    let projecteditor = ProjectEditor.ProjectEditor();
+    ok (projecteditor, "ProjectEditor has been initialized");
+
+    return projecteditor.load(iframe).then(() => {
+      info ("Closing the tab after a load has been requested and finished");
+      gBrowser.removeCurrentTab();
+    });
+  });
+
+  finish();
+});
+
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_init.js
@@ -0,0 +1,18 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that projecteditor can be initialized.
+
+function test() {
+  info ("Initializing projecteditor");
+  addProjectEditorTab().then((projecteditor) => {
+    ok (projecteditor, "Load callback has been called");
+    ok (projecteditor.shells, "ProjectEditor has shells");
+    ok (projecteditor.project, "ProjectEditor has a project");
+    finish();
+  });
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_new_file.js
@@ -0,0 +1,13 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test tree selection functionality
+
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  ok(projecteditor, "ProjectEditor has loaded");
+
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_stores.js
@@ -0,0 +1,16 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test ProjectEditor basic functionality
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  let TEMP_PATH = [...projecteditor.project.allPaths()][0];
+  is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+  is ([...projecteditor.project.allPaths()].length, 1, "1 path is set");
+  projecteditor.project.removeAllStores();
+  is ([...projecteditor.project.allPaths()].length, 0, "No paths are remaining");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js
@@ -0,0 +1,69 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test tree selection functionality
+
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  let TEMP_PATH = [...projecteditor.project.allPaths()][0];
+
+  is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly.");
+
+  ok (projecteditor.currentEditor, "There is an editor for projecteditor");
+  let resources = projecteditor.project.allResources();
+
+  is (
+    resources.map(r=>r.basename).join("|"),
+    "ProjectEditor|css|styles.css|data|img|icons|128x128.png|16x16.png|32x32.png|vector.svg|fake.png|js|script.js|index.html|LICENSE|README.md",
+    "Resources came through in proper order"
+  );
+
+  for (let i = 0; i < resources.length; i++){
+    yield selectFileFirstLoad(projecteditor, resources[i]);
+  }
+  for (let i = 0; i < resources.length; i++){
+    yield selectFileSubsequentLoad(projecteditor, resources[i]);
+  }
+  for (let i = 0; i < resources.length; i++){
+    yield selectFileSubsequentLoad(projecteditor, resources[i]);
+  }
+});
+
+function selectFileFirstLoad(projecteditor, resource) {
+  ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+  projecteditor.projectTree.selectResource(resource);
+
+  if (resource.isDir) {
+    return;
+  }
+
+  let [editorCreated, editorLoaded, editorActivated] = yield promise.all([
+    onceEditorCreated(projecteditor),
+    onceEditorLoad(projecteditor),
+    onceEditorActivated(projecteditor)
+  ]);
+
+  is (editorCreated, projecteditor.currentEditor,  "Editor has been created for " + resource.path);
+  is (editorActivated, projecteditor.currentEditor,  "Editor has been activated for " + resource.path);
+  is (editorLoaded, projecteditor.currentEditor,  "Editor has been loaded for " + resource.path);
+}
+
+function selectFileSubsequentLoad(projecteditor, resource) {
+  ok (resource && resource.path, "A valid resource has been passed in for selection " + (resource && resource.path));
+  projecteditor.projectTree.selectResource(resource);
+
+  if (resource.isDir) {
+    return;
+  }
+
+  // Only activated should fire the next time
+  // (may add load() if we begin checking for changes from disk)
+  let [editorActivated] = yield promise.all([
+    onceEditorActivated(projecteditor)
+  ]);
+
+  is (editorActivated, projecteditor.currentEditor,  "Editor has been activated for " + resource.path);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/head.js
@@ -0,0 +1,255 @@
+/* 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 Cu = Components.utils;
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const TargetFactory = devtools.TargetFactory;
+const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+const promise = devtools.require("sdk/core/promise");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const ProjectEditor = devtools.require("projecteditor/projecteditor");
+
+const TEST_URL_ROOT = "http://mochi.test:8888/browser/browser/devtools/projecteditor/test/";
+const SAMPLE_WEBAPP_URL = TEST_URL_ROOT + "/helper_homepage.html";
+let TEMP_PATH;
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+//Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+// Set the testing flag on gDevTools and reset it when the test ends
+gDevTools.testing = true;
+registerCleanupFunction(() => gDevTools.testing = false);
+
+// Clear preferences that may be set during the course of tests.
+registerCleanupFunction(() => {
+  // Services.prefs.clearUserPref("devtools.dump.emit");
+  TEMP_PATH = null;
+});
+
+// Auto close the toolbox and close the test tabs when the test ends
+registerCleanupFunction(() => {
+  try {
+    let target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.closeToolbox(target);
+  } catch (ex) {
+    dump(ex);
+  }
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+});
+
+/**
+ * Define an async test based on a generator function
+ */
+function asyncTest(generator) {
+  return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
+}
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTab(url) {
+  info("Adding a new tab with URL: '" + url + "'");
+  let def = promise.defer();
+
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    info("URL '" + url + "' loading complete");
+    waitForFocus(() => {
+      def.resolve(tab);
+    }, content);
+  }, true);
+  content.location = url;
+
+  return def.promise;
+}
+