Bug 777085: New markup panel for the inspector. r=jwalker
authorDave Camp <dcamp@mozilla.com>
Thu, 23 Aug 2012 11:00:43 -0700
changeset 105262 844da7b047d2a07984844c795b8cb9d2e529bd07
parent 105158 ad7963c93bd8e334e06433b6357fe0a432f107df
child 105263 1c0ac073dc650d335197c2b14501605de264e4db
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersjwalker
bugs777085
milestone17.0a1
Bug 777085: New markup panel for the inspector. r=jwalker
browser/devtools/Makefile.in
browser/devtools/highlighter/inspector.jsm
browser/devtools/highlighter/test/Makefile.in
browser/devtools/highlighter/test/browser_inspector_bug_690361.js
browser/devtools/highlighter/test/browser_inspector_editor.js
browser/devtools/highlighter/test/browser_inspector_editor_name.js
browser/devtools/highlighter/test/browser_inspector_initialization.js
browser/devtools/highlighter/test/browser_inspector_menu.js
browser/devtools/highlighter/test/browser_inspector_tab_switch.js
browser/devtools/highlighter/test/browser_inspector_treePanel_click.js
browser/devtools/highlighter/test/browser_inspector_treePanel_input.html
browser/devtools/highlighter/test/browser_inspector_treePanel_menu.js
browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html
browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js
browser/devtools/highlighter/test/browser_inspector_treePanel_output.js
browser/devtools/highlighter/test/browser_inspector_treePanel_result.html
browser/devtools/jar.mn
browser/devtools/markupview/Makefile.in
browser/devtools/markupview/MarkupView.jsm
browser/devtools/markupview/markup-view.css
browser/devtools/markupview/markup-view.xhtml
browser/devtools/markupview/test/Makefile.in
browser/devtools/markupview/test/browser_inspector_markup_edit.html
browser/devtools/markupview/test/browser_inspector_markup_edit.js
browser/devtools/markupview/test/browser_inspector_markup_mutation.html
browser/devtools/markupview/test/browser_inspector_markup_mutation.js
browser/devtools/markupview/test/browser_inspector_markup_navigation.html
browser/devtools/markupview/test/browser_inspector_markup_navigation.js
browser/devtools/markupview/test/head.js
browser/devtools/shared/Undo.jsm
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js
browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
browser/themes/gnomestripe/devtools/markup-view.css
browser/themes/gnomestripe/jar.mn
browser/themes/pinstripe/devtools/markup-view.css
browser/themes/pinstripe/jar.mn
browser/themes/winstripe/devtools/markup-view.css
browser/themes/winstripe/jar.mn
--- a/browser/devtools/Makefile.in
+++ b/browser/devtools/Makefile.in
@@ -9,16 +9,17 @@ srcdir    = @srcdir@
 VPATH   = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 include $(topsrcdir)/config/config.mk
 
 DIRS = \
   highlighter \
+  markupview \
   webconsole \
   commandline \
   sourceeditor \
   styleeditor \
   styleinspector \
   tilt \
   scratchpad \
   debugger \
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -9,16 +9,17 @@ const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 var EXPORTED_SYMBOLS = ["InspectorUI"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/TreePanel.jsm");
+Cu.import("resource:///modules/devtools/MarkupView.jsm");
 Cu.import("resource:///modules/highlighter.jsm");
 Cu.import("resource:///modules/devtools/LayoutView.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 
 // Inspector notifications dispatched through the nsIObserverService.
 const INSPECTOR_NOTIFICATIONS = {
   // Fires once the Inspector completes the initialization and opens up on
   // screen.
@@ -64,16 +65,25 @@ const LAYOUT_CHANGE_TIMER = 250;
 function Inspector(aIUI)
 {
   this._IUI = aIUI;
   this._winID = aIUI.winID;
   this._browser = aIUI.browser;
   this._listeners = {};
 
   this._browser.addEventListener("resize", this, true);
+
+  this._markupButton = this._IUI.chromeDoc.getElementById("inspector-treepanel-toolbutton");
+
+  if (Services.prefs.getBoolPref("devtools.inspector.htmlPanelOpen")) {
+    this.openMarkup();
+  } else {
+    this.closeMarkup();
+  }
+
 }
 
 Inspector.prototype = {
   /**
    * True if the highlighter is locked on a node.
    */
   get locked() {
     return !this._IUI.inspecting;
@@ -129,16 +139,17 @@ Inspector.prototype = {
   },
 
   /**
    * Called by the InspectorUI when the inspector is being destroyed.
    */
   _destroy: function Inspector__destroy()
   {
     this._cancelLayoutChange();
+    this._destroyMarkup();
     this._browser.removeEventListener("resize", this, true);
     delete this._IUI;
     delete this._listeners;
   },
 
   /**
    * Event handler for DOM events.
    *
@@ -173,37 +184,169 @@ Inspector.prototype = {
   _cancelLayoutChange: function Inspector_cancelLayoutChange()
   {
     if (this._timer) {
       this._IUI.win.clearTimeout(this._timer);
       delete this._timer;
     }
   },
 
+  toggleMarkup: function Inspector_toggleMarkup()
+  {
+    if (this._markupFrame) {
+      this.closeMarkup();
+      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", false);
+    } else {
+      this.openMarkup(true);
+      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", true);
+    }
+  },
+
+  /**
+   * XXX: The sidebar has an object that exists and is manipulated
+   * separately from its actual loading.  So the public api for
+   * the sidebar looks like:
+   *
+   * if (inspector.sidebar.visible) { inspector.sidebar.close() }
+   *
+   * whereas the markup API looks more like
+   *
+   * if (inspector.markupOpen) { inspector.closeMarkup() }
+   *
+   * Maybe we should add an InspectorMarkup object that presents
+   * the public api for the markup panel?
+   */
+  get markupOpen() {
+    return this._markupOpen;
+  },
+
+  openMarkup: function Inspector_openMarkup(aFocus)
+  {
+    this._markupButton.setAttribute("checked", "true");
+    this._markupOpen = true;
+    if (!this._markupFrame) {
+      this._initMarkup(aFocus);
+    }
+  },
+
+  closeMarkup: function Inspector_closeMarkup()
+  {
+    this._markupButton.removeAttribute("checked");
+    this._markupOpen = false;
+    this._destroyMarkup();
+  },
+
+  _initMarkup: function Inspector_initMarkupPane(aFocus)
+  {
+    let doc = this._IUI.chromeDoc;
+
+    this._markupBox = doc.createElement("vbox");
+    try {
+      this._markupBox.height =
+        Services.prefs.getIntPref("devtools.inspector.htmlHeight");
+    } catch(e) {
+      this._markupBox.height = 112;
+    }
+    this._markupBox.minHeight = 64;
+
+    this._markupSplitter = doc.createElement("splitter");
+    this._markupSplitter.className = "devtools-horizontal-splitter";
+
+    let container = doc.getElementById("appcontent");
+    container.appendChild(this._markupSplitter);
+    container.appendChild(this._markupBox);
+
+    // create tool iframe
+    this._markupFrame = doc.createElement("iframe");
+    this._markupFrame.setAttribute("flex", "1");
+    this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
+
+    // This is needed to enable tooltips inside the iframe document.
+    this._boundMarkupFrameLoad = function Inspector_initMarkupPanel_onload() {
+      if (aFocus) {
+        this._markupFrame.contentWindow.focus();
+      }
+      this._onMarkupFrameLoad();
+    }.bind(this);
+    this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
+
+    this._markupSplitter.setAttribute("hidden", true);
+    this._markupBox.setAttribute("hidden", true);
+    this._markupBox.appendChild(this._markupFrame);
+    this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
+  },
+
+  _onMarkupFrameLoad: function Inspector__onMarkupFrameLoad()
+  {
+    this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
+    delete this._boundMarkupFrameLoad;
+
+    this._markupSplitter.removeAttribute("hidden");
+    this._markupBox.removeAttribute("hidden");
+
+    this.markup = new MarkupView(this, this._markupFrame);
+    this._emit("markuploaded");
+  },
+
+  _destroyMarkup: function Inspector__destroyMarkup()
+  {
+    if (this._boundMarkupFrameLoad) {
+      this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
+      delete this._boundMarkupFrameLoad;
+    }
+
+    if (this.markup) {
+      this.markup.destroy();
+      delete this.markup;
+    }
+
+    if (this._markupFrame) {
+      delete this._markupFrame;
+    }
+
+    if (this._markupBox) {
+      Services.prefs.setIntPref("devtools.inspector.htmlHeight", this._markupBox.height);
+      this._markupBox.parentNode.removeChild(this._markupBox);
+      delete this._markupBox;
+    }
+
+    if (this._markupSplitter) {
+      this._markupSplitter.parentNode.removeChild(this._markupSplitter);
+    }
+  },
+
   /**
    * Called by InspectorUI after a tab switch, when the
    * inspector is no longer the active tab.
    */
   _freeze: function Inspector__freeze()
   {
+    if (this._markupBox) {
+      this._markupSplitter.setAttribute("hidden", true);
+      this._markupBox.setAttribute("hidden", true);
+    }
     this._cancelLayoutChange();
     this._browser.removeEventListener("resize", this, true);
     this._frozen = true;
   },
 
   /**
    * Called by InspectorUI after a tab switch when the
    * inspector is back to being the active tab.
    */
   _thaw: function Inspector__thaw()
   {
     if (!this._frozen) {
       return;
     }
 
+    if (this._markupOpen && !this._boundMarkupFrameLoad) {
+      this._markupSplitter.removeAttribute("hidden");
+      this._markupBox.removeAttribute("hidden");
+    }
     this._browser.addEventListener("resize", this, true);
     delete this._frozen;
   },
 
   /// Event stuff.  Would like to refactor this eventually.
   /// Emulates the jetpack event source, which has a nice API.
 
   /**
@@ -437,25 +580,17 @@ InspectorUI.prototype = {
     }
   },
 
   /**
    * Toggle the TreePanel.
    */
   toggleHTMLPanel: function IUI_toggleHTMLPanel()
   {
-    if (this.treePanel.isOpen()) {
-      this.treePanel.close();
-      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", false);
-      this.currentInspector._htmlPanelOpen = false;
-    } else {
-      this.treePanel.open();
-      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", true);
-      this.currentInspector._htmlPanelOpen = true;
-    }
+    this.currentInspector.toggleMarkup();
   },
 
   /**
    * Is the inspector UI open? Simply check if the toolbar is visible or not.
    *
    * @returns boolean
    */
   get isInspectorOpen()
@@ -549,17 +684,16 @@ InspectorUI.prototype = {
     this.inspectCommand = this.chromeDoc.getElementById("Inspector:Inspect");
 
     // Update menus:
     this.inspectorUICommand = this.chromeDoc.getElementById("Tools:Inspect");
     this.inspectorUICommand.setAttribute("checked", "true");
 
     this.chromeWin.Tilt.setup();
 
-    this.treePanel = new TreePanel(this.chromeWin, this);
     this.toolbar.hidden = false;
 
     // initialize the HTML Breadcrumbs
     this.breadcrumbs = new HTMLBreadcrumbs(this);
 
     this.isDirty = false;
 
     this.progressListener = new InspectorProgressListener(this);
@@ -675,23 +809,16 @@ InspectorUI.prototype = {
    *
    * @param boolean aKeepInspector
    *        Tells if you want the inspector associated to the current tab/window to
    *        be cleared or not. Set this to true to save the inspector, or false
    *        to destroy it.
    */
   closeInspectorUI: function IUI_closeInspectorUI(aKeepInspector)
   {
-    // if currently editing an attribute value, closing the
-    // highlighter/HTML panel dismisses the editor
-    if (this.treePanel && this.treePanel.editingContext)
-      this.treePanel.closeEditor();
-
-    this.treePanel.destroy();
-
     if (this.closing || !this.win || !this.browser) {
       return;
     }
 
     let winId = new String(this.winID); // retain this to notify observers.
 
     this.closing = true;
     this.toolbar.hidden = true;
@@ -747,17 +874,16 @@ InspectorUI.prototype = {
     this.inspectorUICommand.setAttribute("checked", "false");
 
     this.browser = this.win = null; // null out references to browser and window
     this.winID = null;
     this.selection = null;
     this.closing = false;
     this.isDirty = false;
 
-    delete this.treePanel;
     delete this.stylePanel;
     delete this.inspectorUICommand;
     delete this.inspectCommand;
     delete this.toolbar;
 
     Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.CLOSED, null);
 
     if (!aKeepInspector)
@@ -765,21 +891,16 @@ InspectorUI.prototype = {
   },
 
   /**
    * Begin inspecting webpage, attach page event listeners, activate
    * highlighter event listeners.
    */
   startInspecting: function IUI_startInspecting()
   {
-    // if currently editing an attribute value, starting
-    // "live inspection" mode closes the editor
-    if (this.treePanel && this.treePanel.editingContext)
-      this.treePanel.closeEditor();
-
     this.inspectCommand.setAttribute("checked", "true");
 
     this.inspecting = true;
     this.highlighter.unlock();
     this._notifySelected();
     this._currentInspector._emit("unlocked");
   },
 
@@ -823,38 +944,32 @@ InspectorUI.prototype = {
    *        force an update?
    * @param aScroll boolean
    *        scroll the tree panel?
    * @param aFrom [optional] string
    *        which part of the UI the selection occured from
    */
   select: function IUI_select(aNode, forceUpdate, aScroll, aFrom)
   {
-    // if currently editing an attribute value, using the
-    // highlighter dismisses the editor
-    if (this.treePanel && this.treePanel.editingContext)
-      this.treePanel.closeEditor();
-
     if (!aNode)
       aNode = this.defaultSelection;
 
     if (forceUpdate || aNode != this.selection) {
       if (aFrom != "breadcrumbs") {
         this.clearPseudoClassLocks();
       }
 
       this.selection = aNode;
       if (!this.inspecting) {
         this.highlighter.highlight(this.selection);
       }
     }
 
     this.breadcrumbs.update();
     this.chromeWin.Tilt.update(aNode);
-    this.treePanel.select(aNode, aScroll, aFrom);
 
     this._notifySelected(aFrom);
   },
 
   /**
    * Toggle the pseudo-class lock on the currently inspected element. If the
    * pseudo-class is :hover or :active, that pseudo-class will also be toggled
    * on every ancestor of the element, mirroring real :hover and :active
@@ -934,20 +1049,16 @@ InspectorUI.prototype = {
     } else {
       this.highlighter.lock();
     }
 
     Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null);
 
     this.highlighter.highlight();
 
-    if (this.currentInspector._htmlPanelOpen) {
-      this.treePanel.open();
-    }
-
     if (this.currentInspector._sidebarOpen) {
       this._sidebar.show();
     }
 
     let menu = this.chromeDoc.getElementById("inspectorToggleVeil");
     if (this.currentInspector._highlighterShowVeil) {
       menu.setAttribute("checked", "true");
     } else {
@@ -1120,25 +1231,21 @@ InspectorUI.prototype = {
     let root = selection.ownerDocument.documentElement;
     if (selection === root) {
       // We can't delete the root element.
       return;
     }
 
     let parent = selection.parentNode;
 
-    // remove the node from the treepanel
-    if (this.treePanel.isOpen())
-      this.treePanel.deleteChildBox(selection);
-
     // remove the node from content
     parent.removeChild(selection);
     this.breadcrumbs.invalidateHierarchy();
 
-    // select the parent node in the highlighter, treepanel, breadcrumbs
+    // select the parent node in the highlighter and breadcrumbs
     this.inspectNode(parent);
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Utility Methods
 
   /**
    * inspect the given node, highlighting it on the page and selecting the
--- a/browser/devtools/highlighter/test/Makefile.in
+++ b/browser/devtools/highlighter/test/Makefile.in
@@ -13,42 +13,32 @@ include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = \
 		browser_inspector_initialization.js \
 		browser_inspector_treeSelection.js \
 		browser_inspector_highlighter.js \
 		browser_inspector_iframeTest.js \
 		browser_inspector_scrolling.js \
 		browser_inspector_tab_switch.js \
-		browser_inspector_treePanel_output.js \
-		browser_inspector_treePanel_input.html \
-		browser_inspector_treePanel_result.html \
 		browser_inspector_bug_665880.js \
 		browser_inspector_bug_674871.js \
-		browser_inspector_editor.js \
-		browser_inspector_editor_name.js \
 		browser_inspector_bug_566084_location_changed.js \
 		browser_inspector_infobar.js \
 		browser_inspector_bug_690361.js \
 		browser_inspector_bug_672902_keyboard_shortcuts.js \
 		browser_inspector_keybindings.js \
 		browser_inspector_breadcrumbs.html \
 		browser_inspector_breadcrumbs.js \
 		browser_inspector_bug_699308_iframe_navigation.js \
 		browser_inspector_changes.js \
 		browser_inspector_ruleviewstore.js \
 		browser_inspector_invalidate.js \
 		browser_inspector_sidebarstate.js \
-		browser_inspector_treePanel_menu.js \
+		browser_inspector_menu.js \
 		browser_inspector_pseudoclass_lock.js \
 		browser_inspector_pseudoClass_menu.js \
 		browser_inspector_destroyselection.html \
 		browser_inspector_destroyselection.js \
-		browser_inspector_treePanel_navigation.html \
-		browser_inspector_treePanel_navigation.js \
 		head.js \
 		$(NULL)
 
-# Disabled due to constant failures
-# 		browser_inspector_treePanel_click.js \
-
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/highlighter/test/browser_inspector_bug_690361.js
+++ b/browser/devtools/highlighter/test/browser_inspector_bug_690361.js
@@ -38,34 +38,34 @@ function runInspectorTests()
   Services.obs.removeObserver(runInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
   Services.obs.addObserver(closeInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
 
   ok(InspectorUI.toolbar, "we have the toolbar.");
   ok(!InspectorUI.toolbar.hidden, "toolbar is visible");
   ok(InspectorUI.inspecting, "Inspector is inspecting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector._markupOpen, "Inspector Tree Panel is not open");
   ok(InspectorUI.highlighter, "Highlighter is up");
 
   salutation = doc.getElementById("salutation");
   InspectorUI.inspectNode(salutation);
 
   let button = document.getElementById("highlighter-closebutton");
   button.click();
 }
 
 function closeInspectorTests()
 {
   Services.obs.removeObserver(closeInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
   Services.obs.addObserver(inspectorOpenedTrap,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
-  ok(!InspectorUI.isInspectorOpen, "Inspector Tree Panel is not open");
+  ok(!InspectorUI.isInspectorOpen, "Inspector is not open");
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function() {
     gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
     gBrowser.removeCurrentTab();
   }, true);
 
   gBrowser.tabContainer.addEventListener("TabSelect", finishInspectorTests, false);
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_editor.js
+++ /dev/null
@@ -1,273 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-/* ***** BEGIN LICENSE BLOCK *****
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- * ***** END LICENSE BLOCK *****
- */
-
-let doc;
-let div;
-let editorTestSteps;
-
-function doNextStep() {
-  try {
-    editorTestSteps.next();
-  } catch(exception) {
-    info("caught:", exception);
-  }
-}
-
-function setupEditorTests()
-{
-  div = doc.createElement("div");
-  div.setAttribute("id", "foobar");
-  div.setAttribute("class", "barbaz");
-  doc.body.appendChild(div);
-
-  Services.obs.addObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
-  InspectorUI.toggleInspectorUI();
-}
-
-function setupHTMLPanel()
-{
-  Services.obs.removeObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-  Services.obs.addObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-  InspectorUI.toggleHTMLPanel();
-}
-
-function runEditorTests()
-{
-  Services.obs.removeObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-  InspectorUI.stopInspecting();
-  InspectorUI.inspectNode(doc.body, true);
-
-  // setup generator for async test steps
-  editorTestSteps = doEditorTestSteps();
-
-  // add step listeners
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
-
-  // start the tests
-  doNextStep();
-}
-
-function highlighterTrap()
-{
-  // bug 696107
-  InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
-  ok(false, "Highlighter moved. Shouldn't be here!");
-  finishUp();
-}
-
-function doEditorTestSteps()
-{
-  let treePanel = InspectorUI.treePanel;
-  let editor = treePanel.treeBrowserDocument.getElementById("attribute-editor");
-  let editorInput = treePanel.treeBrowserDocument.getElementById("attribute-editor-input");
-
-  // Step 1: grab and test the attribute-value nodes in the HTML panel, then open editor
-  let attrValNode_id = treePanel.treeBrowserDocument.querySelectorAll(".nodeValue.editable[data-attributeName='id']")[0];
-  let attrValNode_class = treePanel.treeBrowserDocument.querySelectorAll(".nodeValue.editable[data-attributeName='class']")[0];
-
-  is(attrValNode_id.innerHTML, "foobar", "Step 1: we have the correct `id` attribute-value node in the HTML panel");
-  is(attrValNode_class.innerHTML, "barbaz", "we have the correct `class` attribute-value node in the HTML panel");
-
-  // double-click the `id` attribute-value node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
-  });
-
-
-  yield; // End of Step 1
-
-  // Step 2: validate editing session, enter new attribute value into editor, and save input
-  ok(InspectorUI.treePanel.editingContext, "Step 2: editor session started");
-  let selection = InspectorUI.selection;
-
-  ok(selection, "Selection is: " + selection);
-
-  let editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  // check if the editor popup is "near" the correct position
-  let editorDims = editor.getBoundingClientRect();
-  let attrValNodeDims = attrValNode_id.getBoundingClientRect();
-  let editorPositionOK = (editorDims.left >= (attrValNodeDims.left - editorDims.width - 5)) &&
-                          (editorDims.right <= (attrValNodeDims.right + editorDims.width + 5)) &&
-                          (editorDims.top >= (attrValNodeDims.top - editorDims.height - 5)) &&
-                          (editorDims.bottom <= (attrValNodeDims.bottom + editorDims.height + 5));
-
-  ok(editorPositionOK, "editor position acceptable");
-
-  // check to make sure the attribute-value node being edited is properly highlighted
-  let attrValNodeHighlighted = attrValNode_id.classList.contains("editingAttributeValue");
-  ok(attrValNodeHighlighted, "`id` attribute-value node is editor-highlighted");
-
-  is(treePanel.editingContext.repObj, div, "editor session has correct reference to div");
-  is(treePanel.editingContext.attrObj, attrValNode_id, "editor session has correct reference to `id` attribute-value node in HTML panel");
-  is(treePanel.editingContext.attrName, "id", "editor session knows correct attribute-name");
-
-  editorInput.value = "Hello World";
-  editorInput.focus();
-
-  InspectorUI.highlighter.addListener("nodeselected", highlighterTrap);
-
-  // hit <enter> to save the textbox value
-  executeSoon(function() {
-    // Extra key to test that keyboard handlers have been removed. bug 696107.
-    EventUtils.synthesizeKey("VK_LEFT", {}, attrValNode_id.ownerDocument.defaultView);
-    EventUtils.synthesizeKey("VK_RETURN", {}, attrValNode_id.ownerDocument.defaultView);
-  });
-
-  // two `yield` statements, to trap both the "SAVED" and "CLOSED" events that will be triggered
-  yield;
-  yield; // End of Step 2
-
-  // remove this from previous step
-  InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
-
-  // Step 3: validate that the previous editing session saved correctly, then open editor on `class` attribute value
-  ok(!treePanel.editingContext, "Step 3: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  attrValNodeHighlighted = attrValNode_id.classList.contains("editingAttributeValue");
-  ok(!attrValNodeHighlighted, "`id` attribute-value node is no longer editor-highlighted");
-  is(div.getAttribute("id"), "Hello World", "`id` attribute-value successfully updated");
-  is(attrValNode_id.innerHTML, "Hello World", "attribute-value node in HTML panel successfully updated");
-
-  // double-click the `class` attribute-value node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrValNode_class, 2, 2, {clickCount: 2}, attrValNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 3
-
-
-  // Step 4: enter value into editor, then hit <escape> to discard it
-  ok(treePanel.editingContext, "Step 4: editor session started");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  is(treePanel.editingContext.attrObj, attrValNode_class, "editor session has correct reference to `class` attribute-value node in HTML panel");
-  is(treePanel.editingContext.attrName, "class", "editor session knows correct attribute-name");
-
-  editorInput.value = "Hello World";
-  editorInput.focus();
-
-  // hit <escape> to discard the inputted value
-  executeSoon(function() {
-    EventUtils.synthesizeKey("VK_ESCAPE", {}, attrValNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 4
-
-
-  // Step 5: validate that the previous editing session discarded correctly, then open editor on `id` attribute value again
-  ok(!treePanel.editingContext, "Step 5: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  is(div.getAttribute("class"), "barbaz", "`class` attribute-value *not* updated");
-  is(attrValNode_class.innerHTML, "barbaz", "attribute-value node in HTML panel *not* updated");
-
-  // double-click the `id` attribute-value node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 5
-
-
-  // Step 6: validate that editor opened again, then test double-click inside of editor (should do nothing)
-  ok(treePanel.editingContext, "Step 6: editor session started");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  // double-click on the editor input box
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(editorInput, 2, 2, {clickCount: 2}, editorInput.ownerDocument.defaultView);
-
-    // since the previous double-click is supposed to do nothing,
-    // wait a brief moment, then move on to the next step
-    executeSoon(function() {
-      doNextStep();
-    });
-  });
-
-  yield; // End of Step 6
-
-
-  // Step 7: validate that editing session is still correct, then enter a value and try a click
-  //         outside of editor (should cancel the editing session)
-  ok(treePanel.editingContext, "Step 7: editor session still going");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup still visible");
-
-  editorInput.value = "all your base are belong to us";
-
-  // single-click the `class` attribute-value node
-  executeSoon(function() {
-    EventUtils.synthesizeMouse(attrValNode_class, 2, 2, {}, attrValNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 7
-
-  // Step 8: validate that the editor was closed and that the editing was not saved
-  ok(!treePanel.editingContext, "Step 8: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  is(div.getAttribute("id"), "Hello World", "`id` attribute-value *not* updated");
-  is(attrValNode_id.innerHTML, "Hello World", "attribute-value node in HTML panel *not* updated");
-  executeSoon(doNextStep);
-
-  yield; // End of Step 8
-
-  // Step 9: Open the Editor and verify that closing the tree panel does not make the
-  // Inspector go cray-cray.
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
-    doNextStep();
-  });
-
-  yield; // End of Step 9
-
-  ok(treePanel.editingContext, "Step 9: editor session started");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup is visible");
-  executeSoon(function() {
-    InspectorUI.toggleHTMLPanel();
-    finishUp();
-  });
-}
-
-function finishUp() {
-  // end of all steps, so clean up
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
-  doc = div = null;
-  InspectorUI.closeInspectorUI();
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
-function test()
-{
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    doc = content.document;
-    waitForFocus(setupEditorTests, content);
-  }, true);
-
-  content.location = "data:text/html,basic tests for html panel attribute-value editor";
-}
-
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_editor_name.js
+++ /dev/null
@@ -1,253 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-/* ***** BEGIN LICENSE BLOCK *****
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- * ***** END LICENSE BLOCK *****
- */
-
-let doc;
-let div;
-let editorTestSteps;
-
-function doNextStep() {
-  editorTestSteps.next();
-}
-
-function setupEditorTests()
-{
-  div = doc.createElement("div");
-  div.setAttribute("id", "foobar");
-  div.setAttribute("class", "barbaz");
-  doc.body.appendChild(div);
-
-  Services.obs.addObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
-  InspectorUI.toggleInspectorUI();
-}
-
-function setupHTMLPanel()
-{
-  Services.obs.removeObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-  Services.obs.addObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-  InspectorUI.toggleHTMLPanel();
-}
-
-function runEditorTests()
-{
-  Services.obs.removeObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-  InspectorUI.stopInspecting();
-  InspectorUI.inspectNode(doc.body, true);
-
-  // setup generator for async test steps
-  editorTestSteps = doEditorTestSteps();
-
-  // add step listeners
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
-  Services.obs.addObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
-
-  // start the tests
-  doNextStep();
-}
-
-function highlighterTrap()
-{
-  // bug 696107
-  InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
-  ok(false, "Highlighter moved. Shouldn't be here!");
-  finishUp();
-}
-
-function doEditorTestSteps()
-{
-  let treePanel = InspectorUI.treePanel;
-  let editor = treePanel.treeBrowserDocument.getElementById("attribute-editor");
-  let editorInput = treePanel.treeBrowserDocument.getElementById("attribute-editor-input");
-
-  // Step 1: grab and test the attribute-name nodes in the HTML panel, then open editor
-  let nodes = treePanel.treeBrowserDocument.querySelectorAll(".nodeName.editable");
-  let attrNameNode_id = nodes[0]
-  let attrNameNode_class = nodes[1];
-
-  is(attrNameNode_id.innerHTML, "id", "Step 1: we have the correct `id` attribute-name node in the HTML panel");
-  is(attrNameNode_class.innerHTML, "class", "we have the correct `class` attribute-name node in the HTML panel");
-
-  // double-click the `id` attribute-name node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrNameNode_id, 2, 2, {clickCount: 2}, attrNameNode_id.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 1
-
-
-  // Step 2: validate editing session, enter new attribute value into editor, and save input
-  ok(InspectorUI.treePanel.editingContext, "Step 2: editor session started");
-  let selection = InspectorUI.selection;
-
-  ok(selection, "Selection is: " + selection);
-
-  let editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  // check if the editor popup is "near" the correct position
-  let editorDims = editor.getBoundingClientRect();
-  let attrNameNodeDims = attrNameNode_id.getBoundingClientRect();
-  let editorPositionOK = (editorDims.left >= (attrNameNodeDims.left - editorDims.width - 5)) &&
-                          (editorDims.right <= (attrNameNodeDims.right + editorDims.width + 5)) &&
-                          (editorDims.top >= (attrNameNodeDims.top - editorDims.height - 5)) &&
-                          (editorDims.bottom <= (attrNameNodeDims.bottom + editorDims.height + 5));
-
-  ok(editorPositionOK, "editor position acceptable");
-
-  // check to make sure the attribute-value node being edited is properly highlighted
-  let attrNameNodeHighlighted = attrNameNode_id.classList.contains("editingAttributeValue");
-  ok(attrNameNodeHighlighted, "`id` attribute-name node is editor-highlighted");
-
-  is(treePanel.editingContext.repObj, div, "editor session has correct reference to div");
-  is(treePanel.editingContext.attrObj, attrNameNode_id, "editor session has correct reference to `id` attribute-name node in HTML panel");
-  is(treePanel.editingContext.attrName, "id", "editor session knows correct attribute-name");
-
-  editorInput.value = "burp";
-  editorInput.focus();
-
-  InspectorUI.highlighter.addListener("nodeselected", highlighterTrap);
-
-  // hit <enter> to save the textbox value
-  executeSoon(function() {
-    // Extra key to test that keyboard handlers have been removed. bug 696107.
-    EventUtils.synthesizeKey("VK_LEFT", {}, attrNameNode_id.ownerDocument.defaultView);
-    EventUtils.synthesizeKey("VK_RETURN", {}, attrNameNode_id.ownerDocument.defaultView);
-  });
-
-  // two `yield` statements, to trap both the "SAVED" and "CLOSED" events that will be triggered
-  yield;
-  yield; // End of Step 2
-
-  // remove this from previous step
-  InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
-
-  // Step 3: validate that the previous editing session saved correctly, then open editor on `class` attribute value
-  ok(!treePanel.editingContext, "Step 3: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  attrNameNodeHighlighted = attrNameNode_id.classList.contains("editingAttributeValue");
-  ok(!attrNameNodeHighlighted, "`id` attribute-value node is no longer editor-highlighted");
-  is(div.getAttribute("burp"), "foobar", "`id` attribute-name successfully updated");
-  is(attrNameNode_id.innerHTML, "burp", "attribute-name node in HTML panel successfully updated");
-
-  // double-click the `class` attribute-value node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrNameNode_class, 2, 2, {clickCount: 2}, attrNameNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 3
-
-
-  // Step 4: enter value into editor, then hit <escape> to discard it
-  ok(treePanel.editingContext, "Step 4: editor session started");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  is(treePanel.editingContext.attrObj, attrNameNode_class, "editor session has correct reference to `class` attribute-name node in HTML panel");
-  is(treePanel.editingContext.attrName, "class", "editor session knows correct attribute-name");
-
-  editorInput.value = "Hello World";
-  editorInput.focus();
-
-  // hit <escape> to discard the inputted value
-  executeSoon(function() {
-    EventUtils.synthesizeKey("VK_ESCAPE", {}, attrNameNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 4
-
-
-  // Step 5: validate that the previous editing session discarded correctly, then open editor on `id` attribute value again
-  ok(!treePanel.editingContext, "Step 5: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  is(div.getAttribute("class"), "barbaz", "`class` attribute-name *not* updated");
-  is(attrNameNode_class.innerHTML, "class", "attribute-name node in HTML panel *not* updated");
-
-  // double-click the `id` attribute-name node to open the editor
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(attrNameNode_id, 2, 2, {clickCount: 2}, attrNameNode_id.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 5
-
-
-  // Step 6: validate that editor opened again, then test double-click inside of editor (should do nothing)
-  ok(treePanel.editingContext, "Step 6: editor session started");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup visible");
-
-  // double-click on the editor input box
-  executeSoon(function() {
-    // firing 2 clicks right in a row to simulate a double-click
-    EventUtils.synthesizeMouse(editorInput, 2, 2, {clickCount: 2}, editorInput.ownerDocument.defaultView);
-
-    // since the previous double-click is supposed to do nothing,
-    // wait a brief moment, then move on to the next step
-    executeSoon(function() {
-      doNextStep();
-    });
-  });
-
-  yield; // End of Step 6
-
-
-  // Step 7: validate that editing session is still correct, then enter a value and try a click
-  //         outside of editor (should cancel the editing session)
-  ok(treePanel.editingContext, "Step 7: editor session still going");
-  editorVisible = editor.classList.contains("editing");
-  ok(editorVisible, "editor popup still visible");
-
-  editorInput.value = "all your base are belong to us";
-
-  // single-click the `class` attribute-value node
-  executeSoon(function() {
-    EventUtils.synthesizeMouse(attrNameNode_class, 2, 2, {}, attrNameNode_class.ownerDocument.defaultView);
-  });
-
-  yield; // End of Step 7
-
-
-  // Step 8: validate that the editor was closed and that the editing was not saved
-  ok(!treePanel.editingContext, "Step 8: editor session ended");
-  editorVisible = editor.classList.contains("editing");
-  ok(!editorVisible, "editor popup hidden");
-  is(div.getAttribute("burp"), "foobar", "`id` attribute-name *not* updated");
-  is(attrNameNode_id.innerHTML, "burp", "attribute-value node in HTML panel *not* updated");
-
-  // End of Step 8
-  executeSoon(finishUp);
-}
-
-function finishUp() {
-  // end of all steps, so clean up
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, false);
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, false);
-  Services.obs.removeObserver(doNextStep, InspectorUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, false);
-  doc = div = null;
-  InspectorUI.closeInspectorUI();
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
-function test()
-{
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    doc = content.document;
-    waitForFocus(setupEditorTests, content);
-  }, true);
-
-  content.location = "data:text/html,basic tests for html panel attribute-value editor";
-}
-
--- a/browser/devtools/highlighter/test/browser_inspector_initialization.js
+++ b/browser/devtools/highlighter/test/browser_inspector_initialization.js
@@ -32,36 +32,33 @@ function startInspectorTests()
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
   InspectorUI.toggleInspectorUI();
 }
 
 function runInspectorTests()
 {
   Services.obs.removeObserver(runInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-  Services.obs.addObserver(treePanelTests,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
 
   ok(InspectorUI.toolbar, "we have the toolbar.");
   ok(!InspectorUI.toolbar.hidden, "toolbar is visible");
   ok(InspectorUI.inspecting, "Inspector is inspecting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.markupOpen, "Inspector Tree Panel is not open");
   ok(!InspectorUI.sidebar.visible, "Inspector sidebar should not visible.");
   ok(InspectorUI.highlighter, "Highlighter is up");
   InspectorUI.inspectNode(doc.body);
   InspectorUI.stopInspecting();
 
-  InspectorUI.treePanel.open();
+  InspectorUI.currentInspector.once("markuploaded", treePanelTests);
+  InspectorUI.currentInspector.openMarkup();
 }
 
 function treePanelTests()
 {
-  Services.obs.removeObserver(treePanelTests,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-  ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
+  ok(InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is open");
 
   InspectorUI.toggleSidebar();
   ok(InspectorUI.sidebar.visible, "Inspector Sidebar should be open");
   InspectorUI.toggleSidebar();
   ok(!InspectorUI.sidebar.visible, "Inspector Sidebar should be closed");
   InspectorUI.sidebar.show();
   InspectorUI.currentInspector.once("sidebaractivated-computedview",
     stylePanelTests)
@@ -165,17 +162,16 @@ function inspectNodesFromContextTestTrap
 
 function finishInspectorTests(subject, topic, aWinIdString)
 {
   Services.obs.removeObserver(finishInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.DESTROYED);
 
   is(parseInt(aWinIdString), winId, "winId of destroyed Inspector matches");
   ok(!InspectorUI.highlighter, "Highlighter is gone");
-  ok(!InspectorUI.treePanel, "Inspector Tree Panel is closed");
   ok(!InspectorUI.inspecting, "Inspector is not inspecting");
   ok(!InspectorUI._sidebar, "Inspector Sidebar is closed");
   ok(!InspectorUI.toolbar, "toolbar is hidden");
 
   Services.obs.removeObserver(inspectNodesFromContextTestTrap, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
   gBrowser.removeCurrentTab();
   finish();
 }
rename from browser/devtools/highlighter/test/browser_inspector_treePanel_menu.js
rename to browser/devtools/highlighter/test/browser_inspector_menu.js
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_menu.js
+++ b/browser/devtools/highlighter/test/browser_inspector_menu.js
@@ -36,20 +36,19 @@ function test() {
 
   function setupTest() {
     Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     InspectorUI.toggleInspectorUI();
   }
 
   function runTests() {
     Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-    Services.obs.addObserver(testCopyInnerMenu, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
     InspectorUI.stopInspecting();
     InspectorUI.inspectNode(node1, true);
-    InspectorUI.treePanel.open();
+    testCopyInnerMenu();
   }
 
   function testCopyInnerMenu() {
     let copyInner = document.getElementById("inspectorHTMLCopyInner");
     ok(copyInner, "the popup menu has a copy inner html menu item");
 
     waitForClipboard("This is some example text",
                      function() { copyInner.doCommand(); },
--- a/browser/devtools/highlighter/test/browser_inspector_tab_switch.js
+++ b/browser/devtools/highlighter/test/browser_inspector_tab_switch.js
@@ -22,17 +22,17 @@ function inspectorTabOpen1()
 
 function inspectorUIOpen1()
 {
   Services.obs.removeObserver(inspectorUIOpen1,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   ok(!InspectorUI.sidebar.visible, "Inspector Sidebar is not open");
   ok(!InspectorUI.store.isEmpty(), "InspectorUI.store is not empty");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   // Highlight a node.
   div = content.document.getElementsByTagName("div")[0];
   InspectorUI.inspectNode(div);
   is(InspectorUI.selection, div, "selection matches the div element");
@@ -49,17 +49,16 @@ function inspectorUIOpen1()
 
   content.location = "data:text/html,<p>tab 2: the inspector should close now";
 }
 
 function inspectorTabOpen2()
 {
   // Make sure the inspector is closed.
   ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-  ok(!InspectorUI.treePanel, "Inspector Tree Panel is closed");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   // Activate the inspector again.
   executeSoon(function() {
     Services.obs.addObserver(inspectorUIOpen2,
       InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     clearUserPrefs();
     InspectorUI.openInspectorUI();
@@ -68,17 +67,17 @@ function inspectorTabOpen2()
 
 function inspectorUIOpen2()
 {
   Services.obs.removeObserver(inspectorUIOpen2,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
 
   // Disable highlighting.
   InspectorUI.toggleInspection();
   ok(!InspectorUI.inspecting, "Inspector is not highlighting");
 
 
   // Switch back to tab 1.
@@ -91,33 +90,28 @@ function inspectorUIOpen2()
 
 function inspectorFocusTab1()
 {
   Services.obs.removeObserver(inspectorFocusTab1,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is still open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
   is(InspectorUI.selection, div, "selection matches the div element");
 
-  Services.obs.addObserver(inspectorOpenTreePanelTab1,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-
+  InspectorUI.currentInspector.once("markuploaded", inspectorOpenTreePanelTab1);
   InspectorUI.toggleHTMLPanel();
 }
 
 function inspectorOpenTreePanelTab1()
 {
-  Services.obs.removeObserver(inspectorOpenTreePanelTab1,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
+  ok(InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
   is(InspectorUI.selection, div, "selection matches the div element");
 
   InspectorUI.currentInspector.once("sidebaractivated-computedview",
     inspectorSidebarStyleView1);
 
   executeSoon(function() {
     InspectorUI.sidebar.show();
@@ -145,43 +139,48 @@ function inspectorSidebarStyleView1()
 
 function inspectorFocusTab2()
 {
   Services.obs.removeObserver(inspectorFocusTab2,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is still open.
   ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   ok(!InspectorUI.sidebar.visible, "Inspector Sidebar is not open");
   is(InspectorUI.store.length, 2, "Inspector.store.length is 2");
   isnot(InspectorUI.selection, div, "selection does not match the div element");
 
 
   executeSoon(function() {
     // Make sure keybindings still work
     synthesizeKeyFromKeyTag("key_inspect");
 
     ok(InspectorUI.inspecting, "Inspector is highlighting");
     InspectorUI.toggleInspection();
 
     // Switch back to tab 1.
     Services.obs.addObserver(inspectorSecondFocusTab1,
-      InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
+      InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     gBrowser.selectedTab = tab1;
   });
 }
 
 function inspectorSecondFocusTab1()
 {
   Services.obs.removeObserver(inspectorSecondFocusTab1,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
+    InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+  InspectorUI.currentInspector.once("sidebaractivated-computedview",
+    inspectorSecondFocusTabSidebarLoaded);
+}
 
+function inspectorSecondFocusTabSidebarLoaded()
+{
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
+  ok(InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
   is(InspectorUI.selection, div, "selection matches the div element");
 
   ok(InspectorUI.sidebar.visible, "Inspector Sidebar is open");
   ok(computedView(), "Inspector Has a Style Panel Instance");
   InspectorUI.sidebar._toolObjects().forEach(function(aTool) {
     let btn = aTool.button;
     is(btn.hasAttribute("checked"),
@@ -197,17 +196,17 @@ function inspectorSecondFocusTab1()
 
 function inspectorSecondFocusTab2()
 {
   Services.obs.removeObserver(inspectorSecondFocusTab2,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
 
   // Make sure the inspector is still open.
   ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open");
 
   is(InspectorUI.store.length, 2, "Inspector.store.length is 2");
   isnot(InspectorUI.selection, div, "selection does not match the div element");
 
   // Remove tab 1.
   tab1window = gBrowser.getBrowserForTab(tab1).contentWindow;
   tab1window.addEventListener("pagehide", inspectorTabUnload1, false);
@@ -216,17 +215,17 @@ function inspectorSecondFocusTab2()
 
 function inspectorTabUnload1(evt)
 {
   tab1window.removeEventListener(evt.type, arguments.callee, false);
   tab1window = tab1 = tab2 = div = null;
 
   // Make sure the Inspector is still open and that the state is correct.
   ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
+  ok(!InspectorUI.currentInspector.markupOpen, "Inspector Tree Panel is not open");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   InspectorUI.closeInspectorUI();
   gBrowser.removeCurrentTab();
   finish();
 }
 
 function test()
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_click.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-http://creativecommons.org/publicdomain/zero/1.0/ */
-
-
-function test() {
-
-  waitForExplicitFinish();
-
-  let doc;
-  let node1;
-  let node2;
-
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
-    doc = content.document;
-    waitForFocus(setupTest, content);
-  }, true);
-
-  content.location = 'data:text/html,<div style="width: 200px; height: 200px"><p></p></div>';
-
-  function setupTest() {
-    node1 = doc.querySelector("div");
-    node2 = doc.querySelector("p");
-    Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
-    InspectorUI.toggleInspectorUI();
-  }
-
-  function runTests() {
-    Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-    Services.obs.addObserver(testNode1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-    InspectorUI.select(node1, true, true, true);
-    InspectorUI.openTreePanel();
-  }
-
-  function testNode1() {
-    Services.obs.removeObserver(testNode1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-    is(InspectorUI.selection, node1, "selection matches node");
-    is(getHighlitNode(), node1, "selection matches node");
-    testNode2();
-  }
-
-  function testNode2() {
-    InspectorUI.highlighter.addListener("nodeselected", testHighlightingNode2);
-    InspectorUI.treePanelSelect("node2");
-  }
-
-  function testHighlightingNode2() {
-    InspectorUI.highlighter.removeListener("nodeselected", testHighlightingNode2);
-    is(InspectorUI.selection, node2, "selection matches node");
-    is(getHighlitNode(), node2, "selection matches node");
-    Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
-    InspectorUI.closeInspectorUI();
-  }
-
-  function finishUp() {
-    Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
-    doc = node1 = node2 = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  }
-}
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_input.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html xml:lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>Inspector tree panel test</title>
-    <style type="text/css"><!--
-      #duplicate { color: green }
-    --></style>
-    <script type="text/javascript"><!--
-      function fooBarBaz(arg1) {
-        return true; // do nothing
-      }
-    // --></script>
-  </head>
-  <body arbitrary:attribute="value">
-    <p>Inspector tree panel test.</p>
-
-    <div id="foo" class="foo bar baz" style="border:1px solid red;
-      unknownProperty: unknownValue; color: withUnkownValue">
-      <unknownTag unknownAttribute="fooBar">
-        <p unknownAttribute="fooBar" data-test1="value">hello world!</p>
-      </unknownTag>
-    </div>
-
-    <div id="duplicate" id="duplicate" id="different" class="test" class="foo"
-      fooBar="baz" fooBar="bazbaz">test</div>
-
-    <iframe src="data:text/html,&lt;div&gt;hello from an iframe!&lt;/div&gt;">no
-      frames!</iframe>
-
-    <!-- hello world from a comment! -->
-  </body>
-</html>
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_output.js
+++ /dev/null
@@ -1,102 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set 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/. */
-
-let doc = null;
-let xhr = null;
-let expectedResult = "";
-
-const TEST_URI = "http://mochi.test:8888/browser/browser/devtools/highlighter/test/browser_inspector_treePanel_input.html";
-const RESULT_URI = "http://mochi.test:8888/browser/browser/devtools/highlighter/test/browser_inspector_treePanel_result.html";
-
-function tabFocused()
-{
-  xhr = new XMLHttpRequest();
-  xhr.onreadystatechange = xhr_onReadyStateChange;
-  xhr.open("GET", RESULT_URI, true);
-  xhr.send(null);
-}
-
-function xhr_onReadyStateChange() {
-  if (xhr.readyState != 4) {
-    return;
-  }
-
-  is(xhr.status, 200, "xhr.status is 200");
-  ok(!!xhr.responseText, "xhr.responseText is available");
-  expectedResult = xhr.responseText.replace(/^\s+|\s+$/mg, '');
-  xhr = null;
-
-  Services.obs.addObserver(inspectorOpened,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
-  InspectorUI.openInspectorUI();
-}
-
-function inspectorOpened()
-{
-  Services.obs.removeObserver(inspectorOpened,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-
-  Services.obs.addObserver(treePanelOpened, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-  InspectorUI.treePanel.open();
-}
-
-function treePanelOpened()
-{
-  Services.obs.removeObserver(treePanelOpened,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
-
-  ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
-  InspectorUI.stopInspecting();
-  ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-
-  let elements = doc.querySelectorAll("meta, script, style, p[unknownAttribute]");
-  for (let i = 0; i < elements.length; i++) {
-    InspectorUI.inspectNode(elements[i]);
-  }
-
-  let iframe = doc.querySelector("iframe");
-  ok(iframe, "Found the iframe tag");
-  ok(iframe.contentDocument, "Found the iframe.contentDocument");
-
-  let iframeDiv = iframe.contentDocument.querySelector("div");
-  ok(iframeDiv, "Found the div element inside the iframe");
-  InspectorUI.inspectNode(iframeDiv);
-
-  ok(InspectorUI.treePanel.treePanelDiv, "InspectorUI.treePanelDiv is available");
-  is(InspectorUI.treePanel.treePanelDiv.innerHTML.replace(/^\s+|\s+$/mg, ''),
-    expectedResult, "treePanelDiv.innerHTML is correct");
-  expectedResult = null;
-
-  Services.obs.addObserver(inspectorClosed,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
-  InspectorUI.closeInspectorUI();
-}
-
-function inspectorClosed()
-{
-  Services.obs.removeObserver(inspectorClosed,
-    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
-
-  ok(!InspectorUI.inspecting, "Inspector is not highlighting");
-  ok(!InspectorUI.treePanel, "Inspector Tree Panel is not open");
-
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
-function test()
-{
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
-    doc = content.document;
-    waitForFocus(tabFocused, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
deleted file mode 100644
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_result.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<div role="presentation" class="nodeBox htmlNodeBox containerNodeBox  repIgnore open"><div class="docType ">&lt;!DOCTYPE html&gt;</div><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">xml:lang</span>="<span data-attributename="xml:lang" class="nodeValue editable ">en</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox emptyNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">meta</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">charset</span>="<span data-attributename="charset" class="nodeValue editable ">utf-8</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">title</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test</span></span>&lt;/<span class="nodeTag ">title</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">style</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span data-attributename="type" class="nodeValue editable ">text/css</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
-#duplicate { color: green }
---&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">style</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">script</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">type</span>="<span data-attributename="type" class="nodeValue editable ">text/javascript</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox "><span class="nodeText editable "><span class="  ">&lt;!--
-function fooBarBaz(arg1) {
-return true; // do nothing
-}
-// --&gt;</span></span></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">script</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">arbitrary:attribute</span>="<span data-attributename="arbitrary:attribute" class="nodeValue editable ">value</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">Inspector tree panel test.</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span data-attributename="id" class="nodeValue editable ">foo</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span data-attributename="class" class="nodeValue editable ">foo bar baz</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">style</span>="<span data-attributename="style" class="nodeValue editable ">border:1px solid red;
-unknownProperty: unknownValue; color: withUnkownValue</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">unknowntag</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span data-attributename="unknownattribute" class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">p</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">data-test1</span>="<span data-attributename="data-test1" class="nodeValue editable ">value</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">unknownattribute</span>="<span data-attributename="unknownattribute" class="nodeValue editable ">fooBar</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello world!</span></span>&lt;/<span class="nodeTag ">p</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">unknowntag</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">id</span>="<span data-attributename="id" class="nodeValue editable ">duplicate</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">class</span>="<span data-attributename="class" class="nodeValue editable ">test</span>"</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">foobar</span>="<span data-attributename="foobar" class="nodeValue editable ">baz</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">test</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">iframe</span><span class="nodeAttr editGroup ">&nbsp;<span class="nodeName editable ">src</span>="<span data-attributename="src" class="nodeValue editable ">data:text/html,&lt;div&gt;hello from an iframe!&lt;/div&gt;</span>"</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">html</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore "><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">head</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  "></span></span>&lt;/<span class="nodeTag ">head</span>&gt;</span></div></div><div role="presentation" class="nodeBox containerNodeBox  repIgnore open"><div role="presentation" class="nodeLabel "><img role="presentation" class="twisty "><span role="treeitem" aria-expanded="true" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">body</span><span class="nodeBracket editable insertBefore ">&gt;</span></span></div><div role="group" class="nodeChildBox "><div role="presentation" class="nodeBox textNodeBox  repIgnore selected"><div role="presentation" class="nodeLabel "><span role="treeitem" class="nodeLabelBox repTarget ">&lt;<span class="nodeTag ">div</span><span class="nodeBracket editable insertBefore ">&gt;</span><span class="nodeText editable "><span class="  ">hello from an iframe!</span></span>&lt;/<span class="nodeTag ">div</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">iframe</span>&gt;</span></div></div><div role="presentation" class="nodeBox nodeComment ">&lt;!--<span class="nodeComment editable "> hello world from a comment! </span>--&gt;</div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">body</span>&gt;</span></div></div></div><div role="presentation" class="nodeCloseLabel "><span class="nodeCloseLabelBox repTarget ">&lt;/<span class="nodeTag ">html</span>&gt;</span></div></div>
\ No newline at end of file
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,14 +1,16 @@
 # 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/inspector.html                (highlighter/inspector.html)
+    content/browser/devtools/markup-view.xhtml    (markupview/markup-view.xhtml)
+    content/browser/devtools/markup-view.css      (markupview/markup-view.css)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/HUDService-content.js (webconsole/HUDService-content.js)
     content/browser/devtools/webconsole.js        (webconsole/webconsole.js)
 *   content/browser/devtools/webconsole.xul       (webconsole/webconsole.xul)
 *   content/browser/scratchpad.xul                (scratchpad/scratchpad.xul)
     content/browser/scratchpad.js                 (scratchpad/scratchpad.js)
     content/browser/splitview.css                 (shared/splitview.css)
     content/browser/styleeditor.xul               (styleeditor/styleeditor.xul)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/Makefile.in
@@ -0,0 +1,18 @@
+#
+# 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/.
+
+DEPTH		= @DEPTH@
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+TEST_DIRS += test
+
+libs::
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/MarkupView.jsm
@@ -0,0 +1,1115 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+/**
+ * Followup bugs to be filed:
+ * - Drag and drop should be implemented.
+ * - Node menu should be implemented.
+ * - editableField could be moved to a shared location.
+ * - I'm willing to consider that judicious use of DOMTemplater could make this
+ *   code easier to maintain.
+ * - ScrollIntoViewIfNeeded seems jumpy, that should be fixed.
+ */
+
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+// Page size for pageup/pagedown
+const PAGE_SIZE = 10;
+
+var EXPORTED_SYMBOLS = ["MarkupView"];
+
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+Cu.import("resource:///modules/devtools/Templater.jsm");
+Cu.import("resource:///modules/devtools/Undo.jsm")
+
+/**
+ * Vocabulary for the purposes of this file:
+ *
+ * MarkupContainer - the structure that holds an editor and its
+ *  immediate children in the markup panel.
+ * Node - A content node.
+ * object.elt - A UI element in the markup panel.
+ */
+
+/**
+ * The markup tree.  Manages the mapping of nodes to MarkupContainers,
+ * updating based on mutations, and the undo/redo bindings.
+ *
+ * @param Inspector aInspector
+ *        The inspector we're watching.
+ * @param iframe aFrame
+ *        An iframe in which the caller has kindly loaded markup-view.xhtml.
+ */
+function MarkupView(aInspector, aFrame)
+{
+  this._inspector = aInspector;
+  this._frame = aFrame;
+  this.doc = this._frame.contentDocument;
+  this._elt = this.doc.querySelector("#root");
+
+  this.undo = new UndoStack();
+  this.undo.installController(this._frame.ownerDocument.defaultView);
+
+  this._containers = new WeakMap();
+
+  this._observer = new this.doc.defaultView.MutationObserver(this._mutationObserver.bind(this));
+
+  this._boundSelect = this._onSelect.bind(this);
+  this._inspector.on("select", this._boundSelect);
+  this._onSelect();
+
+  this._boundKeyDown = this._onKeyDown.bind(this);
+  this._frame.addEventListener("keydown", this._boundKeyDown, false);
+
+  this._boundFocus = this._onFocus.bind(this);
+  this._frame.addEventListener("focus", this._boundFocus, false);
+
+  this._onSelect();
+}
+
+MarkupView.prototype = {
+  _selectedContainer: null,
+
+  /**
+   * Return the selected node.
+   */
+  get selected() {
+    return this._selectedContainer ? this._selectedContainer.node : null;
+  },
+
+  template: function MT_template(aName, aDest, aOptions)
+  {
+    let node = this.doc.getElementById("template-" + aName).cloneNode(true);
+    node.removeAttribute("id");
+    template(node, aDest, aOptions);
+    return node;
+  },
+
+  /**
+   * Get the MarkupContainer object for a given node, or undefined if
+   * none exists.
+   */
+  getContainer: function MT_getContainer(aNode)
+  {
+    return this._containers.get(aNode);
+  },
+
+  /**
+   * Highlight the given element in the markup panel.
+   */
+  _onSelect: function MT__onSelect()
+  {
+    if (this._inspector.selection) {
+      this.showNode(this._inspector.selection);
+    }
+    this.selectNode(this._inspector.selection);
+  },
+
+  /**
+   * Create a TreeWalker to find the next/previous
+   * node for selection.
+   */
+  _selectionWalker: function MT__seletionWalker(aStart)
+  {
+    let walker = this.doc.createTreeWalker(
+      aStart || this._elt,
+      Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
+      function(aElement) {
+        if (aElement.container && aElement.container.visible) {
+          return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+        }
+        return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+      },
+      false
+    );
+    walker.currentNode = this._selectedContainer.elt;
+    return walker;
+  },
+
+  /**
+   * Key handling.
+   */
+  _onKeyDown: function MT__KeyDown(aEvent)
+  {
+    let handled = true;
+
+    // Ignore keystrokes that originated in editors.
+    if (aEvent.target.tagName.toLowerCase() === "input" ||
+        aEvent.target.tagName.toLowerCase() === "textarea") {
+      return;
+    }
+
+    switch(aEvent.keyCode) {
+      case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
+      case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
+        let node = this._selectedContainer.node;
+        let doc = nodeDocument(node);
+        if (node != doc && node != doc.documentElement) {
+          this.deleteNode(this._selectedContainer.node);
+        }
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
+        this.navigate(this._containers.get(this._rootNode.firstChild));
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
+        this.collapseNode(this._selectedContainer.node);
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
+        this.expandNode(this._selectedContainer.node);
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_UP:
+        let prev = this._selectionWalker().previousNode();
+        if (prev) {
+          this.navigate(prev.container);
+        }
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
+        let next = this._selectionWalker().nextNode();
+        if (next) {
+          this.navigate(next.container);
+        }
+        break;
+      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
+        let walker = this._selectionWalker();
+        let selection = this._selectedContainer;
+        for (let i = 0; i < PAGE_SIZE; i++) {
+          let prev = walker.previousNode();
+          if (!prev) {
+            break;
+          }
+          selection = prev.container;
+        }
+        this.navigate(selection);
+        break;
+      }
+      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: {
+        let walker = this._selectionWalker();
+        let selection = this._selectedContainer;
+        for (let i = 0; i < PAGE_SIZE; i++) {
+          let next = walker.nextNode();
+          if (!next) {
+            break;
+          }
+          selection = next.container;
+        }
+        this.navigate(selection);
+        break;
+      }
+      default:
+        handled = false;
+    }
+    if (handled) {
+      aEvent.stopPropagation();
+      aEvent.preventDefault();
+    }
+  },
+
+  /**
+   * Delete a node from the DOM.
+   * This is an undoable action.
+   */
+  deleteNode: function MC__deleteNode(aNode)
+  {
+    let parentNode = aNode.parentNode;
+    let sibling = aNode.nextSibling;
+
+    this.undo.do(function() {
+      parentNode.removeChild(aNode);
+    }, function() {
+      parentNode.insertBefore(aNode, sibling);
+    });
+  },
+
+  /**
+   * If an editable item is focused, select its container.
+   */
+  _onFocus: function MC__onFocus(aEvent) {
+    let parent = aEvent.target;
+    while (!parent.container) {
+      parent = parent.parentNode;
+    }
+    if (parent) {
+      this.navigate(parent.container, true);
+    }
+  },
+
+  /**
+   * Handle a user-requested navigation to a given MarkupContainer,
+   * updating the inspector's currently-selected node.
+   *
+   * @param MarkupContainer aContainer
+   *        The container we're navigating to.
+   * @param aIgnoreFocus aIgnoreFocus
+   *        If falsy, keyboard focus will be moved to the container too.
+   */
+  navigate: function MT__navigate(aContainer, aIgnoreFocus)
+  {
+    if (!aContainer) {
+      return;
+    }
+
+    let node = aContainer.node;
+    this.showNode(node);
+    this.selectNode(node);
+
+    if (this._inspector._IUI.highlighter.isNodeHighlightable(node)) {
+      this._inspector._IUI.select(node, true, false, "treepanel");
+      this._inspector._IUI.highlighter.highlight(node);
+    }
+
+    if (!aIgnoreFocus) {
+      aContainer.focus();
+    }
+  },
+
+  /**
+   * Make sure a node is included in the markup tool.
+   *
+   * @param DOMNode aNode
+   *        The node in the content document.
+   *
+   * @returns MarkupContainer The MarkupContainer object for this element.
+   */
+  importNode: function MT_importNode(aNode, aExpand)
+  {
+    if (!aNode) {
+      return null;
+    }
+
+    if (this._containers.has(aNode)) {
+      return this._containers.get(aNode);
+    }
+
+    this._observer.observe(aNode, {
+      attributes: true,
+      childList: true,
+      characterData: true,
+    });
+
+    let walker = documentWalker(aNode);
+    let parent = walker.parentNode();
+    if (parent) {
+      // Make sure parents of this node are imported too.
+      var container = new MarkupContainer(this, aNode);
+    } else {
+      var container = new RootContainer(this, aNode);
+      this._elt.appendChild(container.elt);
+      this._rootNode = aNode;
+      aNode.addEventListener("load", function MP_watch_contentLoaded(aEvent) {
+        // Fake a childList mutation here.
+        this._mutationObserver([{target: aEvent.target, type: "childList"}]);
+      }.bind(this), true);
+
+    }
+
+    this._containers.set(aNode, container);
+    container.expanded = aExpand;
+
+    this._updateChildren(container);
+
+    if (parent) {
+      this.importNode(parent, true);
+    }
+    return container;
+  },
+
+  /**
+   * Mutation observer used for included nodes.
+   */
+  _mutationObserver: function MT__mutationObserver(aMutations)
+  {
+    for (let mutation of aMutations) {
+      let container = this._containers.get(mutation.target);
+      if (mutation.type === "attributes" || mutation.type === "characterData") {
+        container.update();
+      } else if (mutation.type === "childList") {
+        this._updateChildren(container);
+      }
+    }
+    this._inspector._emit("markupmutation");
+  },
+
+  /**
+   * Make sure the given node's parents are expanded and the
+   * node is scrolled on to screen.
+   */
+  showNode: function MT_showNode(aNode)
+  {
+    this.importNode(aNode);
+    let walker = documentWalker(aNode);
+    let parent;
+    while (parent = walker.parentNode()) {
+      this.expandNode(parent);
+    }
+//    LayoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).elt, false);
+  },
+
+  /**
+   * Expand the container's children.
+   */
+  _expandContainer: function MT__expandContainer(aContainer)
+  {
+    if (aContainer.hasChildren && !aContainer.expanded) {
+      aContainer.expanded = true;
+      this._updateChildren(aContainer);
+    }
+  },
+
+  /**
+   * Expand the node's children.
+   */
+  expandNode: function MT_expandNode(aNode)
+  {
+    let container = this._containers.get(aNode);
+    this._expandContainer(container);
+  },
+
+  /**
+   * Expand the entire tree beneath a container.
+   *
+   * @param aContainer The container to expand.
+   */
+  _expandAll: function MT_expandAll(aContainer)
+  {
+    this._expandContainer(aContainer);
+    let child = aContainer.children.firstChild;
+    while (child) {
+      this._expandAll(child.container);
+      child = child.nextSibling;
+    }
+  },
+
+  /**
+   * Expand the entire tree beneath a node.
+   *
+   * @param aContainer The node to expand, or null
+   *        to start from the top.
+   */
+  expandAll: function MT_expandAll(aNode)
+  {
+    aNode = aNode || this._rootNode;
+    this._expandAll(this._containers.get(aNode));
+  },
+
+  /**
+   * Collapse the node's children.
+   */
+  collapseNode: function MT_collapseNode(aNode)
+  {
+    let container = this._containers.get(aNode);
+    container.expanded = false;
+  },
+
+  /**
+   * Mark the given node selected.
+   */
+  selectNode: function MT_selectNode(aNode)
+  {
+    let container = this._containers.get(aNode);
+    if (this._selectedContainer === container) {
+      return false;
+    }
+    if (this._selectedContainer) {
+      this._selectedContainer.selected = false;
+    }
+    this._selectedContainer = container;
+    if (aNode) {
+      this._selectedContainer.selected = true;
+    }
+
+    this._selectedContainer.focus();
+
+    return true;
+  },
+
+  /**
+   * Make sure all children of the given container's node are
+   * imported and attached to the container in the right order.
+   */
+  _updateChildren: function MT__updateChildren(aContainer)
+  {
+    // Get a tree walker pointing at the first child of the node.
+    let treeWalker = documentWalker(aContainer.node);
+    let child = treeWalker.firstChild();
+    aContainer.hasChildren = !!child;
+    if (aContainer.expanded) {
+      let lastContainer = null;
+      while (child) {
+        let container = this.importNode(child, false);
+
+        // Make sure children are in the right order.
+        let before = lastContainer ? lastContainer.nextSibling : aContainer.children.firstChild;
+        aContainer.children.insertBefore(container.elt, before);
+        lastContainer = container.elt;
+        child = treeWalker.nextSibling();
+      }
+
+      while (aContainer.children.lastChild != lastContainer) {
+        aContainer.children.removeChild(aContainer.children.lastChild);
+      }
+    }
+  },
+
+  /**
+   * Tear down the markup panel.
+   */
+  destroy: function MT_destroy()
+  {
+    this.undo.destroy();
+    delete this.undo;
+
+    this._frame.addEventListener("focus", this._boundFocus, false);
+    delete this._boundFocus;
+
+    this._frame.removeEventListener("keydown", this._boundKeyDown, true);
+    delete this._boundKeyDown;
+
+    this._inspector.removeListener("select", this._boundSelect);
+    delete this._boundSelect;
+
+    delete this._elt;
+
+    delete this._containers;
+    this._observer.disconnect();
+    delete this._observer;
+  }
+};
+
+
+/**
+ * The main structure for storing a document node in the markup
+ * tree.  Manages creation of the editor for the node and
+ * a <ul> for placing child elements, and expansion/collapsing
+ * of the element.
+ *
+ * @param MarkupView aMarkupView
+ *        The markup view that owns this container.
+ * @param DOMNode aNode
+ *        The node to display.
+ */
+function MarkupContainer(aMarkupView, aNode)
+{
+  this.markup = aMarkupView;
+  this.doc = this.markup.doc;
+  this.undo = this.markup.undo;
+  this.node = aNode;
+
+  if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+    this.editor = new TextEditor(this, aNode, "text");
+  } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
+    this.editor = new TextEditor(this, aNode, "comment");
+  } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+    this.editor = new ElementEditor(this, aNode);
+  } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
+    this.editor = new DoctypeEditor(this, aNode);
+  } else {
+    this.editor = new GenericEditor(this.markup, aNode);
+  }
+
+  // The template will fill the following properties
+  this.elt = null;
+  this.expander = null;
+  this.codeBox = null;
+  this.children = null;
+  let options = { stack: "markup-view.xhtml" };
+  this.markup.template("container", this, options);
+
+  this.elt.container = this;
+
+  this.expander.addEventListener("click", function() {
+    this.markup.navigate(this);
+
+    if (this.expanded) {
+      this.markup.collapseNode(this.node);
+    } else {
+      this.markup.expandNode(this.node);
+    }
+  }.bind(this));
+
+  this.codeBox.insertBefore(this.editor.elt, this.children);
+
+  this.editor.elt.addEventListener("mousedown", function(evt) {
+    this.markup.navigate(this);
+  }.bind(this), false);
+
+  if (this.editor.closeElt) {
+    this.codeBox.appendChild(this.editor.closeElt);
+  }
+
+}
+
+MarkupContainer.prototype = {
+  /**
+   * True if the current node has children.  The MarkupView
+   * will set this attribute for the MarkupContainer.
+   */
+  _hasChildren: false,
+
+  get hasChildren() {
+    return this._hasChildren;
+  },
+
+  set hasChildren(aValue) {
+    this._hasChildren = aValue;
+    if (aValue) {
+      this.expander.style.visibility = "visible";
+    } else {
+      this.expander.style.visibility = "hidden";
+    }
+  },
+
+  /**
+   * True if the node has been visually expanded in the tree.
+   */
+  get expanded() {
+    return this.children.hasAttribute("expanded");
+  },
+
+  set expanded(aValue) {
+    if (aValue) {
+      this.expander.setAttribute("expanded", "");
+      this.children.setAttribute("expanded", "");
+    } else {
+      this.expander.removeAttribute("expanded");
+      this.children.removeAttribute("expanded");
+    }
+  },
+
+  /**
+   * True if the container is visible in the markup tree.
+   */
+  get visible()
+  {
+    return this.elt.getBoundingClientRect().height > 0;
+  },
+
+  /**
+   * True if the container is currently selected.
+   */
+  _selected: false,
+
+  get selected() {
+    return this._selected;
+  },
+
+  set selected(aValue) {
+    this._selected = aValue;
+    if (this._selected) {
+      this.editor.elt.classList.add("selected");
+      if (this.editor.closeElt) {
+        this.editor.closeElt.classList.add("selected");
+      }
+    } else {
+      this.editor.elt.classList.remove("selected");
+      if (this.editor.closeElt) {
+        this.editor.closeElt.classList.remove("selected");
+      }
+    }
+  },
+
+  /**
+   * Update the container's editor to the current state of the
+   * viewed node.
+   */
+  update: function MC_update()
+  {
+    if (this.editor.update) {
+      this.editor.update();
+    }
+  },
+
+  /**
+   * Try to put keyboard focus on the current editor.
+   */
+  focus: function MC_focus()
+  {
+    let focusable = this.editor.elt.querySelector("[tabindex]");
+    if (focusable) {
+      focusable.focus();
+    }
+  }
+}
+
+/**
+ * Dummy container node used for the root document element.
+ */
+function RootContainer(aMarkupView, aNode)
+{
+  this.doc = aMarkupView.doc;
+  this.elt = this.doc.createElement("ul");
+  this.children = this.elt;
+  this.node = aNode;
+}
+
+/**
+ * Creates an editor for simple nodes.
+ */
+function GenericEditor(aContainer, aNode)
+{
+  this.elt = aContainer.doc.createElement("span");
+  this.elt.className = "editor";
+  this.elt.textContent = aNode.nodeName;
+}
+
+/**
+ * Creates an editor for a DOCTYPE node.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param DOMNode aNode The node being edited.
+ */
+function DoctypeEditor(aContainer, aNode)
+{
+  this.elt = aContainer.doc.createElement("span");
+  this.elt.className = "editor comment";
+  this.elt.textContent = '<!DOCTYPE ' + aNode.name +
+     (aNode.publicId ? ' PUBLIC "' +  aNode.publicId + '"': '') +
+     (aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
+     '>';
+}
+
+/**
+ * Creates a simple text editor node, used for TEXT and COMMENT
+ * nodes.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param DOMNode aNode The node being edited.
+ * @param string aTemplate The template id to use to build the editor.
+ */
+function TextEditor(aContainer, aNode, aTemplate)
+{
+  this.node = aNode;
+
+  aContainer.markup.template(aTemplate, this);
+
+  _editableField({
+    element: this.value,
+    stopOnReturn: true,
+    trigger: "dblclick",
+    multiline: true,
+    done: function TE_done(aVal, aCommit) {
+      if (!aCommit) {
+        return;
+      }
+      let oldValue = this.node.nodeValue;
+      aContainer.undo.do(function() {
+        this.node.nodeValue = aVal;
+      }.bind(this), function() {
+        this.node.nodeValue = oldValue;
+      }.bind(this));
+    }.bind(this)
+  });
+
+  this.update();
+}
+
+TextEditor.prototype = {
+  update: function TE_update()
+  {
+    this.value.textContent = this.node.nodeValue;
+  }
+};
+
+/**
+ * Creates an editor for an Element node.
+ *
+ * @param MarkupContainer aContainer The container owning this editor.
+ * @param Element aNode The node being edited.
+ */
+function ElementEditor(aContainer, aNode)
+{
+  this.doc = aContainer.doc;
+  this.undo = aContainer.undo;
+  this.template = aContainer.markup.template.bind(aContainer.markup);
+  this.node = aNode;
+
+  this.attrs = [];
+
+  // The templates will fill the following properties
+  this.elt = null;
+  this.tag = null;
+  this.attrList = null;
+  this.newAttr = null;
+  this.closeElt = null;
+  let options = { stack: "markup-view.xhtml" };
+
+  // Create the main editor
+  this.template("element", this, options);
+
+  // Create the closing tag
+  this.template("elementClose", this, options);
+
+  // Make the tag name editable (unless this is a document element)
+  if (aNode != aNode.ownerDocument.documentElement) {
+    this.tag.setAttribute("tabindex", "0");
+    _editableField({
+      element: this.tag,
+      trigger: "dblclick",
+      stopOnReturn: true,
+      done: this.onTagEdit.bind(this),
+    });
+  }
+
+  // Make the new attribute space editable.
+  _editableField({
+    element: this.newAttr,
+    trigger: "dblclick",
+    stopOnReturn: true,
+    done: function EE_onNew(aVal, aCommit) {
+      if (!aCommit) {
+        return;
+      }
+
+      this._applyAttributes(aVal);
+    }.bind(this)
+  });
+
+  let tagName = this.node.nodeName.toLowerCase();
+  this.tag.textContent = tagName;
+  this.closeTag.textContent = tagName;
+
+  this.update();
+}
+
+ElementEditor.prototype = {
+  /**
+   * Update the state of the editor from the node.
+   */
+  update: function EE_update()
+  {
+    let attrs = this.node.attributes;
+    if (!attrs) {
+      return;
+    }
+
+    // Hide all the attribute editors, they'll be re-shown if they're
+    // still applicable.  Don't update attributes that are being
+    // actively edited.
+    let attrEditors = this.attrList.querySelectorAll(".attreditor");
+    for (let i = 0; i < attrEditors.length; i++) {
+      if (!attrEditors[i].inplaceEditor) {
+        attrEditors[i].style.display = "none";
+      }
+    }
+
+    // Get the attribute editor for each attribute that exists on
+    // the node and show it.
+    for (let i = 0; i < attrs.length; i++) {
+      let attr = this._createAttribute(attrs[i]);
+      if (!attr.inplaceEditor) {
+        attr.style.removeProperty("display");
+      }
+    }
+  },
+
+  _createAttribute: function EE_createAttribute(aAttr, aBefore)
+  {
+    if (aAttr.name in this.attrs) {
+      var attr = this.attrs[aAttr.name];
+      var name = attr.querySelector(".attrname");
+      var val = attr.querySelector(".attrvalue");
+    } else {
+      // Create the template editor, which will save some variables here.
+      let data = {
+        attrName: aAttr.name,
+      };
+      let options = { stack: "markup-view.xhtml" };
+      this.template("attribute", data, options);
+      var {attr, inner, name, val} = data;
+
+      // Figure out where we should place the attribute.
+      let before = aBefore || null;
+      if (aAttr.name == "id") {
+        before = this.attrList.firstChild;
+      } else if (aAttr.name == "class") {
+        let idNode = this.attrs["id"];
+        before = idNode ? idNode.nextSibling : this.attrList.firstChild;
+      }
+      this.attrList.insertBefore(attr, before);
+
+      // Make the attribute editable.
+      _editableField({
+        element: inner,
+        trigger: "dblclick",
+        stopOnReturn: true,
+        selectAll: false,
+        start: function EE_editAttribute_start(aEditor, aEvent) {
+          // If the editing was started inside the name or value areas,
+          // select accordingly.
+          if (aEvent.target === name) {
+            aEditor.input.setSelectionRange(0, name.textContent.length);
+          } else if (aEvent.target === val) {
+            let length = val.textContent.length;
+            let editorLength = aEditor.input.value.length;
+            let start = editorLength - (length + 1);
+            aEditor.input.setSelectionRange(start, start + length);
+          } else {
+            aEditor.input.select();
+          }
+        },
+        done: function EE_editAttribute_done(aVal, aCommit) {
+          if (!aCommit) {
+            return;
+          }
+
+          this.undo.startBatch();
+
+          // Remove the attribute stored in this editor and re-add any attributes
+          // parsed out of the input element.
+          this._removeAttribute(this.node, aAttr.name)
+          this._applyAttributes(aVal, attr);
+
+          this.undo.endBatch();
+        }.bind(this)
+      });
+
+      this.attrs[aAttr.name] = attr;
+    }
+
+    name.textContent = aAttr.name;
+    val.textContent = aAttr.value;
+
+    return attr;
+  },
+
+  /**
+   * Parse a user-entered attribute string and apply the resulting
+   * attributes to the node.  This operation is undoable.
+   *
+   * @param string aValue the user-entered value.
+   * @param Element aAttrNode the attribute editor that created this
+   *        set of attributes, used to place new attributes where the
+   *        user put them.
+   */
+  _applyAttributes: function EE__applyAttributes(aValue, aAttrNode)
+  {
+    // Create a dummy node for parsing the attribute list.
+    let dummyNode = this.doc.createElement("div");
+
+    let parseTag = (this.node.namespaceURI.match(/svg/i) ? "svg" :
+                   (this.node.namespaceURI.match(/mathml/i) ? "math" : "div"));
+    let parseText = "<" + parseTag + " " + aValue + "/>";
+    dummyNode.innerHTML = parseText;
+    let parsedNode = dummyNode.firstChild;
+
+    let attrs = parsedNode.attributes;
+
+    this.undo.startBatch();
+
+    for (let i = 0; i < attrs.length; i++) {
+      // Create an attribute editor next to the current attribute if needed.
+      this._createAttribute(attrs[i], aAttrNode ? aAttrNode.nextSibling : null);
+      this._setAttribute(this.node, attrs[i].name, attrs[i].value);
+    }
+
+    this.undo.endBatch();
+  },
+
+  /**
+   * Helper function for _setAttribute and _removeAttribute,
+   * returns a function that puts an attribute back the way it was.
+   */
+  _restoreAttribute: function EE_restoreAttribute(aNode, aName)
+  {
+    if (aNode.hasAttribute(aName)) {
+      let oldValue = aNode.getAttribute(aName);
+      return function() { aNode.setAttribute(aName, oldValue); };
+    } else {
+      return function() { aNode.removeAttribute(aName) };
+    }
+  },
+
+  /**
+   * Sets an attribute.  This operation is undoable.
+   */
+  _setAttribute: function EE_setAttribute(aNode, aName, aValue)
+  {
+    this.undo.do(function() {
+      aNode.setAttribute(aName, aValue);
+    }, this._restoreAttribute(aNode, aName));
+  },
+
+  /**
+   * Removes an attribute.  This operation is undoable.
+   */
+  _removeAttribute: function EE_removeAttribute(aNode, aName)
+  {
+    this.undo.do(function() {
+      aNode.removeAttribute(aName);
+    }, this._restoreAttribute(aNode, aName));
+  },
+
+  /**
+   * Handler for the new attribute editor.
+   */
+  _onNewAttribute: function EE_onNewAttribute(aValue, aCommit)
+  {
+    if (!aValue || !aCommit) {
+      return;
+    }
+
+    this._setAttribute(this.node, aValue, "");
+    let attr = this._createAttribute({ name: aValue, value: ""});
+    attr.style.removeAttribute("display");
+    attr.querySelector("attrvalue").click();
+  },
+
+
+  /**
+   * Called when the tag name editor has is done editing.
+   */
+  onTagEdit: function EE_onTagEdit(aVal, aCommit) {
+    if (!aCommit || aVal == this.node.tagName) {
+      return;
+    }
+
+    // Create a new element with the same attributes as the
+    // current element and prepare to replace the current node
+    // with it.
+    let newElt = nodeDocument(this.node).createElement(aVal);
+    let attrs = this.node.attributes;
+
+    for (let i = 0 ; i < attrs.length; i++) {
+      newElt.setAttribute(attrs[i].name, attrs[i].value);
+    }
+
+    function swapNodes(aOld, aNew) {
+      while (aOld.firstChild) {
+        aNew.appendChild(aOld.firstChild);
+      }
+      aOld.parentNode.insertBefore(aNew, aOld);
+      aOld.parentNode.removeChild(aOld);
+    }
+
+    // Queue an action to swap out the element.
+    this.undo.do(function() {
+      swapNodes(this.node, newElt);
+    }.bind(this), function() {
+      swapNodes(newElt, this.node);
+    }.bind(this));
+  },
+}
+
+
+
+RootContainer.prototype = {
+  hasChildren: true,
+  expanded: true,
+  update: function RC_update() {}
+};
+
+function documentWalker(node) {
+  return new DocumentWalker(node, Ci.nsIDOMNodeFilter.SHOW_ALL, whitespaceTextFilter, false);
+}
+
+function nodeDocument(node) {
+  return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
+}
+
+/**
+ * Similar to a TreeWalker, except will dig in to iframes and it doesn't
+ * implement the good methods like previousNode and nextNode.
+ *
+ * See TreeWalker documentation for explanations of the methods.
+ */
+function DocumentWalker(aNode, aShow, aFilter, aExpandEntityReferences)
+{
+  let doc = nodeDocument(aNode);
+  this.walker = doc.createTreeWalker(nodeDocument(aNode),
+    aShow, aFilter, aExpandEntityReferences);
+  this.walker.currentNode = aNode;
+  this.filter = aFilter;
+}
+
+DocumentWalker.prototype = {
+  get node() this.walker.node,
+  get whatToShow() this.walker.whatToShow,
+  get expandEntityReferences() this.walker.expandEntityReferences,
+  get currentNode() this.walker.currentNode,
+  set currentNode(aVal) this.walker.currentNode = aVal,
+
+  /**
+   * Called when the new node is in a different document than
+   * the current node, creates a new treewalker for the document we've
+   * run in to.
+   */
+  _reparentWalker: function DW_reparentWalker(aNewNode) {
+    if (!aNewNode) {
+      return null;
+    }
+    let doc = nodeDocument(aNewNode);
+    let walker = doc.createTreeWalker(doc,
+      this.whatToShow, this.filter, this.expandEntityReferences);
+    walker.currentNode = aNewNode;
+    this.walker = walker;
+    return aNewNode;
+  },
+
+  parentNode: function DW_parentNode()
+  {
+    let currentNode = this.walker.currentNode;
+    let parentNode = this.walker.parentNode();
+
+    if (!parentNode) {
+      if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
+          && currentNode.defaultView) {
+        let embeddingFrame = currentNode.defaultView.frameElement;
+        if (embeddingFrame) {
+          return this._reparentWalker(embeddingFrame);
+        }
+      }
+      return null;
+    }
+
+    return parentNode;
+  },
+
+  firstChild: function DW_firstChild()
+  {
+    let node = this.walker.currentNode;
+    if (!node)
+      return;
+    if (node.contentDocument) {
+      return this._reparentWalker(node.contentDocument);
+    } else if (node instanceof nodeDocument(node).defaultView.GetSVGDocument) {
+      return this._reparentWalker(node.getSVGDocument());
+    }
+    return this.walker.firstChild();
+  },
+
+  lastChild: function DW_lastChild()
+  {
+    let node = this.walker.currentNode;
+    if (!node)
+      return;
+    if (node.contentDocument) {
+      return this._reparentWalker(node.contentDocument);
+    } else if (node instanceof nodeDocument(node).defaultView.GetSVGDocument) {
+      return this._reparentWalker(node.getSVGDocument());
+    }
+    return this.walker.lastChild();
+  },
+
+  previousSibling: function DW_previousSibling() this.walker.previousSibling(),
+  nextSibling: function DW_nextSibling() this.walker.nextSibling(),
+
+  // XXX bug 785143: not doing previousNode or nextNode, which would sure be useful.
+}
+
+/**
+ * A tree walker filter for avoiding empty whitespace text nodes.
+ */
+function whitespaceTextFilter(aNode)
+{
+    if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
+        !/[^\s]/.exec(aNode.nodeValue)) {
+      return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+    } else {
+      return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/markup-view.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+ul {
+  list-style: none;
+}
+
+ul.children:not([expanded]) {
+  display: none;
+}
+
+.codebox {
+  display: inline-block;
+}
+
+.newattr {
+  display: inline-block;
+  width: 1em;
+  height: 1ex;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -0,0 +1,35 @@
+<?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 html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/markup-view.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
+</head>
+<body role="application">
+  <div id="root"></div>
+  <div id="templates" style="display:none">
+  	<ul>
+  	  <li id="template-container" save="${elt}" class="container"><span save="${expander}" class="expander"></span><span save="${codeBox}" class="codebox"><ul save="${children}" class="children"></ul></span></li>
+    </ul>
+
+    <span id="template-element" save="${elt}" class="editor"><span>&lt;</span><span save="${tag}" class="tagname"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span>&gt;</span>
+
+    <span id="template-attribute" save="${attr}" data-attr="${attrName}" class="attreditor" style="display:none"> <span class="editable" save="${inner}" tabindex="0"><span save="${name}" class="attrname"></span>=&quot;<span save="${val}" class="attrvalue"></span>&quot;</span></span>
+
+    <span id="template-text" save="${elt}" class="editor">
+      <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
+    </span>
+
+    <span id="template-comment" save="${elt}" class="editor comment">
+      <span>&lt;!--</span><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><span>--&gt;</span>
+    </span>
+
+    <span id="template-elementClose" save="${closeElt}">&lt;/<span save="${closeTag}" class="tagname"></span>&gt;</span>
+   </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -0,0 +1,25 @@
+# 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/.
+
+DEPTH     = @DEPTH@
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_FILES = \
+		browser_inspector_markup_navigation.html \
+		browser_inspector_markup_navigation.js \
+		browser_inspector_markup_mutation.html \
+		browser_inspector_markup_mutation.js \
+		browser_inspector_markup_edit.html \
+		browser_inspector_markup_edit.js \
+		head.js \
+		$(NULL)
+
+libs::	$(_BROWSER_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+
+<html class="html">
+
+  <body class="body">
+    <div class="node0">
+      <div id="node1" class="node1">line1</div>
+      <div id="node2" class="node2">line2</div>
+      <p class="node3">line3</p>
+      <!-- A comment -->
+      <p id="node4" class="node4">line4
+        <span class="node5">line5</span>
+        <span class="node6">line6</span>
+        <!-- A comment -->
+        <a class="node7">line7<span class="node8">line8</span></a>
+        <span class="node9">line9</span>
+        <span class="node10">line10</span>
+        <span class="node11">line11</span>
+        <a class="node12">line12<span class="node13">line13</span></a>
+      </p>
+      <p id="node14">line14</p>
+      <p class="node15">line15</p>
+    </div>
+    <div id="node16">
+      <p id="node17">line17</p>
+    </div>
+    <div id="node18">
+      <div id="node19">
+        <div id="node20">
+          <div id="node21">
+            line21
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -0,0 +1,223 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that various editors work as expected.  Also checks
+ * that the various changes are properly undoable and redoable.
+ * For each step in the test, we:
+ * - Check that the node we're editing is as we expect
+ * - Make the change, check that the change was made as we expect
+ * - Undo the change, check that the node is back in its original state
+ * - Redo the change, check that the node change was made again correctly.
+ *
+ * This test mostly tries to verify that the editor makes changes to the
+ * underlying DOM, not that the UI updates - UI updates are based on
+ * underlying DOM changes, and the mutation tests should cover those cases.
+ */
+
+function test() {
+  let tempScope = {}
+  Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
+  let inplaceEditor = tempScope._getInplaceEditorForSpan;
+
+  waitForExplicitFinish();
+
+  // Will hold the doc we're viewing
+  let doc;
+
+  // Holds the MarkupTool object we're testing.
+  let markup;
+
+  /**
+   * Edit a given editableField
+   */
+  function editField(aField, aValue)
+  {
+    aField.focus();
+    EventUtils.sendKey("return");
+    let input = inplaceEditor(aField).input;
+    input.value = aValue;
+    input.blur();
+  }
+
+  function assertAttributes(aElement, aAttributes)
+  {
+    let attrs = Object.getOwnPropertyNames(aAttributes);
+    is(aElement.attributes.length, attrs.length, "Node has the correct number of attributes");
+    for (let attr of attrs) {
+      is(aElement.getAttribute(attr), aAttributes[attr], "Node has the correct " + attr + " attribute.");
+    }
+  }
+
+  // All the mutation types we want to test.
+  let edits = [
+    // Change an attribute
+    {
+      before: function() {
+        assertAttributes(doc.querySelector("#node1"), {
+          id: "node1",
+          class: "node1"
+        });
+      },
+      execute: function() {
+        let editor = markup.getContainer(doc.querySelector("#node1")).editor;
+        let attr = editor.attrs["class"].querySelector(".editable");
+        editField(attr, 'class="changednode1"');
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node1"), {
+          id: "node1",
+          class: "changednode1"
+        });
+      }
+    },
+
+    // Remove an attribute
+    {
+      before: function() {
+        assertAttributes(doc.querySelector("#node4"), {
+          id: "node4",
+          class: "node4"
+        });
+      },
+      execute: function() {
+        let editor = markup.getContainer(doc.querySelector("#node4")).editor;
+        let attr = editor.attrs["class"].querySelector(".editable");
+        editField(attr, '');
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node4"), {
+          id: "node4",
+        });
+      }
+    },
+
+    // Add an attribute by clicking the empty space after a node
+    {
+      before: function() {
+        assertAttributes(doc.querySelector("#node14"), {
+          id: "node14",
+        });
+      },
+      execute: function() {
+        let editor = markup.getContainer(doc.querySelector("#node14")).editor;
+        let attr = editor.newAttr;
+        editField(attr, 'class="newclass" style="color:green"');
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node14"), {
+          id: "node14",
+          class: "newclass",
+          style: "color:green"
+        });
+      }
+    },
+
+    // Add attributes by adding to an existing attribute's entry
+    {
+      before: function() {
+        assertAttributes(doc.querySelector("#node18"), {
+          id: "node18",
+        });
+      },
+      execute: function() {
+        let editor = markup.getContainer(doc.querySelector("#node18")).editor;
+        let attr = editor.attrs["id"].querySelector(".editable");
+        editField(attr, attr.textContent + ' class="newclass" style="color:green"');
+      },
+      after: function() {
+        assertAttributes(doc.querySelector("#node18"), {
+          id: "node18",
+          class: "newclass",
+          style: "color:green"
+        });
+      }
+    },
+
+    // Remove an element with the delete key
+    {
+      before: function() {
+        ok(!!doc.querySelector("#node18"), "Node 18 should exist.");
+      },
+      execute: function() {
+        markup.selectNode(doc.querySelector("#node18"));
+        EventUtils.sendKey("delete");
+      },
+      after: function() {
+        ok(!doc.querySelector("#node18"), "Node 18 should not exist.")
+      }
+    },
+
+    // Edit text
+    {
+      before: function() {
+        let node = doc.querySelector('.node6').firstChild;
+        is(node.nodeValue, "line6", "Text should be unchanged");
+      },
+      execute: function() {
+        let node = doc.querySelector('.node6').firstChild;
+        let editor = markup.getContainer(node).editor;
+        let field = editor.elt.querySelector("pre");
+        editField(field, "New text");
+      },
+      after: function() {
+        let node = doc.querySelector('.node6').firstChild;
+        is(node.nodeValue, "New text", "Text should be changed.");
+      },
+    }
+  ];
+
+  // Create the helper tab for parsing...
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
+
+  function setupTest() {
+    Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+    InspectorUI.toggleInspectorUI();
+  }
+
+  function runTests() {
+    Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+    InspectorUI.currentInspector.once("markuploaded", startTests);
+    InspectorUI.select(doc.body, true, true, true);
+    InspectorUI.stopInspecting();
+    InspectorUI.toggleHTMLPanel();
+  }
+
+  function startTests() {
+    let startNode = doc.documentElement.cloneNode();
+    markup = InspectorUI.currentInspector.markup;
+    markup.expandAll();
+    for (let step of edits) {
+      step.before();
+      step.execute();
+      step.after();
+      ok(markup.undo.canUndo(), "Should be able to undo.");
+      markup.undo.undo();
+      step.before();
+      ok(markup.undo.canRedo(), "Should be able to redo.");
+      markup.undo.redo();
+      step.after();
+    }
+    while (markup.undo.canUndo()) {
+      markup.undo.undo();
+    }
+    // By now we should have a healthy undo stack, clear it out and we should be back where
+    // we started.
+    ok(doc.documentElement.isEqualNode(startNode), "Clearing the undo stack should leave us where we started.");
+    Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+    InspectorUI.closeInspectorUI();
+  }
+
+  function finishUp() {
+    Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
+    doc = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_mutation.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+
+<html class="html">
+
+  <body class="body">
+    <div class="node0">
+      <div id="node1" class="node1">line1</div>
+      <div id="node2" class="node2">line2</div>
+      <p class="node3">line3</p>
+      <!-- A comment -->
+      <p id="node4" class="node4">line4
+        <span class="node5">line5</span>
+        <span class="node6">line6</span>
+        <!-- A comment -->
+        <a class="node7">line7<span class="node8">line8</span></a>
+        <span class="node9">line9</span>
+        <span class="node10">line10</span>
+        <span class="node11">line11</span>
+        <a class="node12">line12<span class="node13">line13</span></a>
+      </p>
+      <p id="node14">line14</p>
+      <p class="node15">line15</p>
+    </div>
+    <div id="node16">
+      <p id="node17">line17</p>
+    </div>
+    <div id="node18">
+      <div id="node19">
+        <div id="node20">
+          <div id="node21">
+            line21
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_mutation.js
@@ -0,0 +1,186 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that various mutations to the dom update the markup tool correctly.
+ * The test for comparing the markup tool to the real dom is a bit weird:
+ * - Select the text in the markup tool
+ * - Parse that as innerHTML in a document we've created for the purpose.
+ * - Remove extraneous whitespace in that tree
+ * - Compare it to the real dom with isEqualNode.
+ */
+
+function test() {
+  waitForExplicitFinish();
+
+  // Will hold the doc we're viewing
+  let contentTab;
+  let doc;
+
+  // Holds the MarkupTool object we're testing.
+  let markup;
+
+  // Holds the document we use to help re-parse the markup tool's output.
+  let parseTab;
+  let parseDoc;
+
+  // Strip whitespace from a node and its children.
+  function stripWhitespace(node)
+  {
+    node.normalize();
+    let iter = node.ownerDocument.createNodeIterator(node, NodeFilter.SHOW_TEXT + NodeFilter.SHOW_COMMENT,
+      null, false);
+
+    while ((node = iter.nextNode())) {
+      node.nodeValue = node.nodeValue.replace(/\s+/g, '');
+      if (node.nodeType == Node.TEXT_NODE &&
+        !/[^\s]/.exec(node.nodeValue)) {
+        node.parentNode.removeChild(node);
+      }
+    }
+  }
+
+  // Verify that the markup in the tool is the same as the markup in the document.
+  function checkMarkup()
+  {
+    markup.expandAll();
+
+    let contentNode = doc.querySelector("body");
+    let panelNode = markup._containers.get(contentNode).elt;
+    let parseNode = parseDoc.querySelector("body");
+
+    // Grab the text from the markup panel...
+    let sel = panelNode.ownerDocument.defaultView.getSelection();
+    sel.selectAllChildren(panelNode);
+
+    // Parse it
+    parseNode.outerHTML = sel;
+    parseNode = parseDoc.querySelector("body");
+
+    // Pull whitespace out of text and comment nodes, there will
+    // be minor unimportant differences.
+    stripWhitespace(parseNode);
+
+    ok(contentNode.isEqualNode(parseNode), "Markup panel should match document.");
+  }
+
+  // All the mutation types we want to test.
+  let mutations = [
+    // Add an attribute
+    function() {
+      let node1 = doc.querySelector("#node1");
+      node1.setAttribute("newattr", "newattrval");
+    },
+    function() {
+      let node1 = doc.querySelector("#node1");
+      node1.removeAttribute("newattr");
+    },
+    function() {
+      let node1 = doc.querySelector("#node1");
+      node1.textContent = "newtext";
+    },
+    function() {
+      let node2 = doc.querySelector("#node2");
+      node2.innerHTML = "<div><span>foo</span></div>";
+    },
+
+    function() {
+      let node4 = doc.querySelector("#node4");
+      while (node4.firstChild) {
+        node4.removeChild(node4.firstChild);
+      }
+    },
+    function() {
+      // Move a child to a new parent.
+      let node17 = doc.querySelector("#node17");
+      let node1 = doc.querySelector("#node2");
+      node1.appendChild(node17);
+    },
+
+    function() {
+      // Swap a parent and child element, putting them in the same tree.
+      // body
+      //  node1
+      //  node18
+      //    node19
+      //      node20
+      //        node21
+      // will become:
+      // body
+      //   node1
+      //     node20
+      //      node21
+      //      node18
+      //        node19
+      let node18 = doc.querySelector("#node18");
+      let node20 = doc.querySelector("#node20");
+
+      let node1 = doc.querySelector("#node1");
+
+      node1.appendChild(node20);
+      node20.appendChild(node18);
+    },
+  ];
+
+  // Create the helper tab for parsing...
+  parseTab = gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    parseDoc = content.document;
+
+    // Then create the actual dom we're inspecting...
+    contentTab = gBrowser.selectedTab = gBrowser.addTab();
+    gBrowser.selectedBrowser.addEventListener("load", function onload2() {
+      gBrowser.selectedBrowser.removeEventListener("load", onload2, true);
+      doc = content.document;
+      // Strip whitespace from the doc for easier comparison.
+      stripWhitespace(doc.documentElement);
+      waitForFocus(setupTest, content);
+    }, true);
+    content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_mutation.html";
+  }, true);
+
+  content.location = "data:text/html,<html></html>";
+
+  function setupTest() {
+    Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+    InspectorUI.toggleInspectorUI();
+  }
+
+  function runTests() {
+    Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+    InspectorUI.currentInspector.once("markuploaded", startTests);
+    InspectorUI.select(doc.body, true, true, true);
+    InspectorUI.stopInspecting();
+    InspectorUI.toggleHTMLPanel();
+  }
+
+  function startTests() {
+    markup = InspectorUI.currentInspector.markup;
+    checkMarkup();
+    nextStep(0);
+  }
+
+  function nextStep(cursor) {
+    if (cursor >= mutations.length) {
+      Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+      InspectorUI.closeInspectorUI();
+      return;
+    }
+    mutations[cursor]();
+    InspectorUI.currentInspector.once("markupmutation", function() {
+      executeSoon(function() {
+        checkMarkup();
+        nextStep(cursor + 1);
+      });
+    });
+  }
+
+  function finishUp() {
+    Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
+    doc = null;
+    gBrowser.removeTab(contentTab);
+    gBrowser.removeTab(parseTab);
+    finish();
+  }
+}
rename from browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html
rename to browser/devtools/markupview/test/browser_inspector_markup_navigation.html
rename from browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js
rename to browser/devtools/markupview/test/browser_inspector_markup_navigation.js
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_navigation.js
@@ -1,68 +1,107 @@
-/* Any copyright is dedicated to the Public Domain.
+/* Any copyright", " is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */
 
 
 function test() {
 
   waitForExplicitFinish();
 
   let doc;
 
-  let keySequence = "right down right ";
-  keySequence += "down down down down right ";
-  keySequence += "down down down right ";
-  keySequence += "down down down down down right ";
-  keySequence += "down down down down down ";
-  keySequence += "up up up left down home ";
-  keySequence += "pagedown left down down pageup pageup left down";
-
-  keySequence = keySequence.split(" ");
-
-  let keySequenceRes = "body node0 node0 ";
-  keySequenceRes += "node1 node2 node3 node4 node4 ";
-  keySequenceRes += "node5 node6 node7 node7 ";
-  keySequenceRes += "node8 node9 node10 node11 node12 node12 ";
-  keySequenceRes += "node13 node14 node15 node15 node15 ";
-  keySequenceRes += "node14 node13 node12 node12 node14 html ";
-  keySequenceRes += "node7 node7 node9 node10 body html html html";
-
-  keySequenceRes = keySequenceRes.split(" ");
-
+  let keySequences = [
+    ["right", "body"],
+    ["down", "node0"],
+    ["right", "node0"],
+    ["down", "node1"],
+    ["down", "node2"],
+    ["down", "node3"],
+    ["down", "*comment*"],
+    ["down", "node4"],
+    ["right", "node4"],
+    ["down", "*text*"],
+    ["down", "node5"],
+    ["down", "node6"],
+    ["down", "*comment*"],
+    ["down" , "node7"],
+    ["right", "node7"],
+    ["down", "*text*"],
+    ["down", "node8"],
+    ["down", "node9"],
+    ["down", "node10"],
+    ["down", "node11"],
+    ["down", "node12"],
+    ["right", "node12"],
+    ["down", "*text*"],
+    ["down", "node13"],
+    ["down", "node14"],
+    ["down", "node15"],
+    ["down", "node15"],
+    ["down", "node15"],
+    ["up", "node14"],
+    ["up", "node13"],
+    ["up", "*text*"],
+    ["up", "node12"],
+    ["left", "node12"],
+    ["down", "node14"],
+    ["home", "*doctype*"],
+    ["pagedown", "*text*"],
+    ["down", "node5"],
+    ["down", "node6"],
+    ["down", "*comment*"],
+    ["down", "node7"],
+    ["left", "node7"],
+    ["down", "node9"],
+    ["down", "node10"],
+    ["pageup", "node2"],
+    ["pageup", "*doctype*"],
+    ["down", "html"],
+    ["left", "html"],
+    ["down", "html"]
+  ];
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onload() {
     gBrowser.selectedBrowser.removeEventListener("load", onload, true);
     doc = content.document;
     waitForFocus(setupTest, content);
   }, true);
 
-  content.location = "http://mochi.test:8888/browser/browser/devtools/highlighter/test/browser_inspector_treePanel_navigation.html";
+  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_navigation.html";
+
+  let markup = null;
 
   function setupTest() {
     Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     InspectorUI.toggleInspectorUI();
   }
 
   function runTests() {
     Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-    Services.obs.addObserver(startNavigation, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
+    InspectorUI.currentInspector.once("markuploaded", startNavigation);
     InspectorUI.select(doc.body, true, true, true);
     InspectorUI.toggleHTMLPanel();
   }
 
   function startNavigation() {
-    Services.obs.removeObserver(startNavigation, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
+    markup = InspectorUI.currentInspector.markup;
     nextStep(0);
   }
 
   function nextStep(cursor) {
-    let key = keySequence[cursor];
-    let className = keySequenceRes[cursor];
+    if (cursor >= keySequences.length) {
+      Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+      InspectorUI.closeInspectorUI();
+      return;
+    }
+
+    let key = keySequences[cursor][0];
+    let className = keySequences[cursor][1];
+
     switch(key) {
       case "right":
         EventUtils.synthesizeKey("VK_RIGHT", {});
         break;
       case "down":
         EventUtils.synthesizeKey("VK_DOWN", {});
         break;
       case "left":
@@ -78,24 +117,27 @@ function test() {
         EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
         break;
       case "home":
         EventUtils.synthesizeKey("VK_HOME", {});
         break;
     }
 
     executeSoon(function() {
-      if (cursor >= keySequence.length) {
-        Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
-        InspectorUI.closeInspectorUI();
+      let node = markup.selected;
+      if (className == "*comment*") {
+        is(node.nodeType, Node.COMMENT_NODE, "[" + cursor + "] should be a comment after moving " + key);
+      } else if (className == "*text*") {
+        is(node.nodeType, Node.TEXT_NODE, "[" + cursor + "] should be text after moving " + key);
+      } else if (className == "*doctype*") {
+        is(node.nodeType, Node.DOCUMENT_TYPE_NODE, "[" + cursor + "] should be doctype after moving " + key);
       } else {
-        let node = InspectorUI.treePanel.ioBox.selectedObjectBox.repObject;
-        is(node.className, className, "[" + cursor + "] right node selected: " + className);
-        nextStep(cursor + 1);
+        is(node.className, className, "[" + cursor + "] right node selected: " + className + " after moving " + key);
       }
+      nextStep(cursor + 1);
     });
   }
 
   function finishUp() {
     Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
     doc = null;
     gBrowser.removeCurrentTab();
     finish();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/head.js
@@ -0,0 +1,15 @@
+/* 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;
+
+// Clear preferences that may be set during the course of tests.
+function clearUserPrefs()
+{
+  Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
+  Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+  Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
+}
+
+registerCleanupFunction(clearUserPrefs);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/Undo.jsm
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 8; 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 = Components.utils;
+
+var EXPORTED_SYMBOLS=["UndoStack"];
+
+/**
+ * A simple undo stack manager.
+ *
+ * Actions are added along with the necessary code to
+ * reverse the action.
+ *
+ * @param function aChange Called whenever the size or position
+ *   of the undo stack changes, to use for updating undo-related
+ *   UI.
+ * @param integer aMaxUndo Maximum number of undo steps.
+ *   defaults to 50.
+ */
+function UndoStack(aChange, aMaxUndo)
+{
+  this.maxUndo = aMaxUndo || 50;
+  this._stack = [];
+}
+
+UndoStack.prototype = {
+  // Current index into the undo .
+  _index: 0,
+
+  // The current batch depth (see startBatch() for details)
+  _batchDepth: 0,
+
+  destroy: function Undo_destroy()
+  {
+    this.uninstallController();
+    delete this._stack;
+  },
+
+  /**
+   * Start a collection of related changes.  Changes will be batched
+   * together into one undo/redo item until endBatch() is called.
+   *
+   * Batches can be nested, in which case the outer batch will contain
+   * all items from the inner batches.  This allows larger user
+   * actions made up of a collection of smaller actions to be
+   * undone as a single action.
+   */
+  startBatch: function Undo_startBatch()
+  {
+    if (this._batchDepth++ === 0) {
+      this._batch = [];
+    }
+  },
+
+  /**
+   * End a batch of related changes, performing its action and adding
+   * it to the undo stack.
+   */
+  endBatch: function Undo_endBatch()
+  {
+    if (--this._batchDepth > 0) {
+      return;
+    }
+
+    // Cut off the undo stack wherever we currently are.
+    let start = Math.max(++this._index - this.maxUndo, 0);
+    this._stack = this._stack.slice(start, this._index);
+
+    let batch = this._batch;
+    delete this._batch;
+    let entry = {
+      do: function() {
+        for (let item of batch) {
+          item.do();
+        }
+      },
+      undo: function() {
+        for (let i = batch.length - 1; i >= 0; i--) {
+          batch[i].undo();
+        }
+      }
+    };
+    this._stack.push(entry);
+    entry.do();
+    this._change();
+  },
+
+  /**
+   * Perform an action, adding it to the undo stack.
+   *
+   * @param function aDo Called to perform the action.
+   * @param function aUndo Called to reverse the action.
+   */
+  do: function Undo_do(aDo, aUndo) {
+    this.startBatch();
+    this._batch.push({ do: aDo, undo: aUndo });
+    this.endBatch();
+  },
+
+  /*
+   * Returns true if undo() will do anything.
+   */
+  canUndo: function Undo_canUndo()
+  {
+    return this._index > 0;
+  },
+
+  /**
+   * Undo the top of the undo stack.
+   *
+   * @return true if an action was undone.
+   */
+  undo: function Undo_canUndo()
+  {
+    if (!this.canUndo()) {
+      return false;
+    }
+    this._stack[--this._index].undo();
+    this._change();
+    return true;
+  },
+
+  /**
+   * Returns true if redo() will do anything.
+   */
+  canRedo: function Undo_canRedo()
+  {
+    return this._stack.length >= this._index;
+  },
+
+  /**
+   * Redo the most recently undone action.
+   *
+   * @return true if an action was redone.
+   */
+  redo: function Undo_canRedo()
+  {
+    if (!this.canRedo()) {
+      return false;
+    }
+    this._stack[this._index++].do();
+    this._change();
+    return true;
+  },
+
+  _change: function Undo__change()
+  {
+    if (this._controllerWindow) {
+      this._controllerWindow.goUpdateCommand("cmd_undo");
+      this._controllerWindow.goUpdateCommand("cmd_redo");
+    }
+  },
+
+  /**
+   * ViewController implementation for undo/redo.
+   */
+
+  /**
+   * Install this object as a command controller.
+   */
+  installController: function Undo_installController(aControllerWindow)
+  {
+    this._controllerWindow = aControllerWindow;
+    aControllerWindow.controllers.appendController(this);
+  },
+
+  /**
+   * Uninstall this object from the command controller.
+   */
+  uninstallController: function Undo_uninstallController()
+  {
+    if (!this._controllerWindow) {
+      return;
+    }
+    this._controllerWindow.controllers.removeController(this);
+  },
+
+  supportsCommand: function Undo_supportsCommand(aCommand)
+  {
+    return (aCommand == "cmd_undo" ||
+            aCommand == "cmd_redo");
+  },
+
+  isCommandEnabled: function Undo_isCommandEnabled(aCommand)
+  {
+    switch(aCommand) {
+      case "cmd_undo": return this.canUndo();
+      case "cmd_redo": return this.canRedo();
+    };
+    return false;
+  },
+
+  doCommand: function Undo_doCommand(aCommand)
+  {
+    switch(aCommand) {
+      case "cmd_undo": return this.undo();
+      case "cmd_redo": return this.redo();
+    }
+  },
+
+  onEvent: function Undo_onEvent(aEvent) {},
+}
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -28,16 +28,17 @@ const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)
 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/CssLogic.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var EXPORTED_SYMBOLS = ["CssRuleView",
                         "_ElementStyle",
+                        "editableItem",
                         "_editableField",
                         "_getInplaceEditorForSpan"];
 
 /**
  * Our model looks like this:
  *
  * ElementStyle:
  *   Responsible for keeping track of which properties are overridden.
@@ -1388,17 +1389,17 @@ RuleEditor.prototype = {
 
     this.closeBrace = createChild(code, "div", {
       class: "ruleview-ruleclose",
       tabindex: "0",
       textContent: "}"
     });
 
     // Create a property editor when the close brace is clicked.
-    editableItem(this.closeBrace, function(aElement) {
+    editableItem({ element: this.closeBrace }, function(aElement) {
       this.newProperty();
     }.bind(this));
   },
 
   /**
    * Update the rule editor with the contents of the rule.
    */
   populate: function RuleEditor_populate()
@@ -1833,103 +1834,117 @@ TextPropertyEditor.prototype = {
  * be focused and create an InlineEditor to handle text input.
  * Changes will be committed when the InlineEditor's input is blurred
  * or dropped when the user presses escape.
  *
  * @param {object} aOptions
  *    Options for the editable field, including:
  *    {Element} element:
  *      (required) The span to be edited on focus.
+ *    {function} canEdit:
+ *       Will be called before creating the inplace editor.  Editor
+ *       won't be created if canEdit returns false.
  *    {function} start:
  *       Will be called when the inplace editor is initialized.
  *    {function} change:
  *       Will be called when the text input changes.  Will be called
  *       with the current value of the text input.
  *    {function} done:
  *       Called when input is committed or blurred.  Called with
  *       current value and a boolean telling the caller whether to
  *       commit the change.  This function is called before the editor
  *       has been torn down.
  *    {function} destroy:
  *       Called when the editor is destroyed and has been torn down.
  *    {string} advanceChars:
  *       If any characters in advanceChars are typed, focus will advance
  *       to the next element.
+ *    {boolean} stopOnReturn:
+ *       If true, the return key will not advance the editor to the next
+ *       focusable element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
  */
 function editableField(aOptions)
 {
-  editableItem(aOptions.element, function(aElement) {
-    new InplaceEditor(aOptions);
+  return editableItem(aOptions, function(aElement, aEvent) {
+    new InplaceEditor(aOptions, aEvent);
   });
 }
 
 /**
  * Handle events for an element that should respond to
  * clicks and sit in the editing tab order, and call
  * a callback when it is activated.
  *
- * @param DOMElement aElement
- *        The DOM element.
+ * @param object aOptions
+ *    The options for this editor, including:
+ *    {Element} element: The DOM element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
  * @param function aCallback
  *        Called when the editor is activated.
  */
-
-function editableItem(aElement, aCallback)
+function editableItem(aOptions, aCallback)
 {
-  aElement.addEventListener("click", function(evt) {
+  let trigger = aOptions.trigger || "click"
+  let element = aOptions.element;
+  element.addEventListener(trigger, function(evt) {
     let win = this.ownerDocument.defaultView;
     let selection = win.getSelection();
-    if (selection.isCollapsed) {
-      aCallback(aElement);
+    if (trigger != "click" || selection.isCollapsed) {
+      aCallback(element, evt);
     }
     evt.stopPropagation();
   }, false);
 
   // If focused by means other than a click, start editing by
   // pressing enter or space.
-  aElement.addEventListener("keypress", function(evt) {
+  element.addEventListener("keypress", function(evt) {
     if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
         evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
-      aCallback(aElement);
+      aCallback(element);
     }
   }, true);
 
   // Ugly workaround - the element is focused on mousedown but
   // the editor is activated on click/mouseup.  This leads
   // to an ugly flash of the focus ring before showing the editor.
   // So hide the focus ring while the mouse is down.
-  aElement.addEventListener("mousedown", function(evt) {
+  element.addEventListener("mousedown", function(evt) {
     let cleanup = function() {
-      aElement.style.removeProperty("outline-style");
-      aElement.removeEventListener("mouseup", cleanup, false);
-      aElement.removeEventListener("mouseout", cleanup, false);
+      element.style.removeProperty("outline-style");
+      element.removeEventListener("mouseup", cleanup, false);
+      element.removeEventListener("mouseout", cleanup, false);
     };
-    aElement.style.setProperty("outline-style", "none");
-    aElement.addEventListener("mouseup", cleanup, false);
-    aElement.addEventListener("mouseout", cleanup, false);
+    element.style.setProperty("outline-style", "none");
+    element.addEventListener("mouseup", cleanup, false);
+    element.addEventListener("mouseout", cleanup, false);
   }, false);
 
   // Mark the element editable field for tab
   // navigation while editing.
-  aElement._editable = true;
+  element._editable = true;
 }
 
 var _editableField = editableField;
 
-function InplaceEditor(aOptions)
+function InplaceEditor(aOptions, aEvent)
 {
   this.elt = aOptions.element;
   let doc = this.elt.ownerDocument;
   this.doc = doc;
   this.elt.inplaceEditor = this;
 
   this.change = aOptions.change;
   this.done = aOptions.done;
   this.destroy = aOptions.destroy;
   this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+  this.multiline = aOptions.multiline || false;
+  this.stopOnReturn = !!aOptions.stopOnReturn;
 
   this._onBlur = this._onBlur.bind(this);
   this._onKeyPress = this._onKeyPress.bind(this);
   this._onInput = this._onInput.bind(this);
 
   this._createInput();
   this._autosize();
 
@@ -1941,32 +1956,35 @@ function InplaceEditor(aOptions)
     this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
   }
 
   // Hide the provided element and add our editor.
   this.originalDisplay = this.elt.style.display;
   this.elt.style.display = "none";
   this.elt.parentNode.insertBefore(this.input, this.elt);
 
-  this.input.select();
+  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
+    this.input.select();
+  }
   this.input.focus();
 
   this.input.addEventListener("blur", this._onBlur, false);
   this.input.addEventListener("keypress", this._onKeyPress, false);
   this.input.addEventListener("input", this._onInput, false);
+  this.input.addEventListener("mousedown", function(aEvt) { aEvt.stopPropagation(); }, false);
 
   if (aOptions.start) {
-    aOptions.start();
+    aOptions.start(this, aEvent);
   }
 }
 
 InplaceEditor.prototype = {
   _createInput: function InplaceEditor_createEditor()
   {
-    this.input = this.doc.createElementNS(HTML_NS, "input");
+    this.input = this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
     this.input.inplaceEditor = this;
     this.input.classList.add("styleinspector-propertyeditor");
     this.input.value = this.initial;
 
     copyTextStyles(this.elt, this.input);
   },
 
   /**
@@ -2006,17 +2024,17 @@ InplaceEditor.prototype = {
   {
     // Create a hidden, absolutely-positioned span to measure the text
     // in the input.  Boo.
 
     // We can't just measure the original element because a) we don't
     // change the underlying element's text ourselves (we leave that
     // up to the client), and b) without tweaking the style of the
     // original element, it might wrap differently or something.
-    this._measurement = this.doc.createElementNS(HTML_NS, "span");
+    this._measurement = this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
     this._measurement.className = "autosizer";
     this.elt.parentNode.appendChild(this._measurement);
     let style = this._measurement.style;
     style.visibility = "hidden";
     style.position = "absolute";
     style.top = "0";
     style.left = "0";
     copyTextStyles(this.input, this._measurement);
@@ -2045,16 +2063,25 @@ InplaceEditor.prototype = {
     // will be wrong.
     this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
 
     // We add a bit of padding to the end.  Should be enough to fit
     // any letter that could be typed, otherwise we'll scroll before
     // we get a chance to resize.  Yuck.
     let width = this._measurement.offsetWidth + 10;
 
+    if (this.multiline) {
+      // Make sure there's some content in the current line.  This is a hack to account
+      // for the fact that after adding a newline the <pre> doesn't grow unless there's
+      // text content on the line.
+      width += 15;
+      this._measurement.textContent += "M";
+      this.input.style.height = this._measurement.offsetHeight + "px";
+    }
+
     this.input.style.width = width + "px";
   },
 
   /**
    * Call the client's done handler and clear out.
    */
   _apply: function InplaceEditor_apply(aEvent)
   {
@@ -2078,34 +2105,41 @@ InplaceEditor.prototype = {
   {
     this._apply();
     this._clear();
   },
 
   _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
   {
     let prevent = false;
-    if (aEvent.charCode in this._advanceCharCodes
+    if (this.multiline &&
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
+        aEvent.shiftKey) {
+      prevent = false;
+    } else if (aEvent.charCode in this._advanceCharCodes
        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
       prevent = true;
 
       let direction = FOCUS_FORWARD;
       if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
           aEvent.shiftKey) {
         this.cancelled = true;
         direction = FOCUS_BACKWARD;
       }
+      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+        direction = null;
+      }
 
       let input = this.input;
 
       this._apply();
 
       let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-      if (fm.focusedElement === input) {
+      if (direction !== null && fm.focusedElement === input) {
         // If the focused element wasn't changed by the done callback,
         // move the focus as requested.
         let next = moveFocus(this.doc.defaultView, direction);
 
         // If the next node to be focused has been tagged as an editable
         // node, send it a click event to trigger
         if (next && next.ownerDocument === this.doc && next._editable) {
           next.click();
--- a/browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_734259_style_editor_link.js
@@ -47,17 +47,16 @@ function openInspector()
 
 function inspectorUIOpen()
 {
   Services.obs.removeObserver(inspectorUIOpen,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
   ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open");
   ok(!InspectorUI.store.isEmpty(), "InspectorUI.store is not empty");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   // Highlight a node.
   let div = content.document.getElementsByTagName("div")[0];
   InspectorUI.inspectNode(div);
   InspectorUI.stopInspecting();
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
@@ -53,17 +53,16 @@ function openInspector()
 
 function inspectorUIOpen()
 {
   Services.obs.removeObserver(inspectorUIOpen,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   // Make sure the inspector is open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
-  ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
   ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open");
   ok(!InspectorUI.store.isEmpty(), "InspectorUI.store is not empty");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   // Highlight a node.
   let div = content.document.getElementsByTagName("div")[0];
   InspectorUI.inspectNode(div);
   InspectorUI.stopInspecting();
new file mode 100644
--- /dev/null
+++ b/browser/themes/gnomestripe/devtools/markup-view.css
@@ -0,0 +1,71 @@
+/* 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/. */
+
+* {
+  padding: 0;
+  margin: 0;
+}
+
+body {
+  font: message-box;
+  background-color: #131c26;
+  color: #8fa1b2;
+}
+
+.tagname {
+  color: #a673bf;
+}
+
+.attrname {
+  color: #b26b47;
+}
+
+.attrvalue {
+  color: #3689b2;
+}
+
+.newattr {
+  cursor: pointer;
+}
+
+.comment {
+  color: #5c6773;
+}
+
+.selected {
+  background-color: #253847;
+}
+
+/* Give some padding to focusable elements to match the editor input
+ * that will replace them. */
+span[tabindex] {
+  display: inline-block;
+  padding: 1px 0;
+}
+
+li.container {
+  position: relative;
+  padding: 2px 0 0 2px;
+}
+
+.codebox {
+  padding-left: 14px;
+}
+
+.expander {
+  position: absolute;
+  -moz-appearance: treetwisty;
+  top: 0;
+  left: 0;
+  width: 14px;
+  height: 14px;
+}
+
+.expander[expanded] {
+  -moz-appearance: treetwistyopen;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+}
--- a/browser/themes/gnomestripe/jar.mn
+++ b/browser/themes/gnomestripe/jar.mn
@@ -104,16 +104,17 @@ browser.jar:
   skin/classic/browser/devtools/alerticon-warning.png (devtools/alerticon-warning.png)
   skin/classic/browser/devtools/goto-mdn.png          (devtools/goto-mdn.png)
   skin/classic/browser/devtools/csshtmltree.css       (devtools/csshtmltree.css)
   skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
   skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.png                  (devtools/webconsole.png)
   skin/classic/browser/devtools/gcli.css              (devtools/gcli.css)
   skin/classic/browser/devtools/htmlpanel.css         (devtools/htmlpanel.css)
+  skin/classic/browser/devtools/markup-view.css      (devtools/markup-view.css)
   skin/classic/browser/devtools/orion.css             (devtools/orion.css)
   skin/classic/browser/devtools/orion-container.css   (devtools/orion-container.css)
   skin/classic/browser/devtools/orion-task.png        (devtools/orion-task.png)
   skin/classic/browser/devtools/orion-breakpoint.png  (devtools/orion-breakpoint.png)
   skin/classic/browser/devtools/orion-debug-location.png (devtools/orion-debug-location.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png                 (devtools/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end-pressed.png              (devtools/breadcrumbs/ltr-end-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end-selected-pressed.png     (devtools/breadcrumbs/ltr-end-selected-pressed.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/pinstripe/devtools/markup-view.css
@@ -0,0 +1,71 @@
+/* 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/. */
+
+* {
+  padding: 0;
+  margin: 0;
+}
+
+body {
+  font: message-box;
+  background-color: #131c26;
+  color: #8fa1b2;
+}
+
+.tagname {
+  color: #a673bf;
+}
+
+.attrname {
+  color: #b26b47;
+}
+
+.attrvalue {
+  color: #3689b2;
+}
+
+.newattr {
+  cursor: pointer;
+}
+
+.comment {
+  color: #5c6773;
+}
+
+.selected {
+  background-color: #253847;
+}
+
+/* Give some padding to focusable elements to match the editor input
+ * that will replace them. */
+span[tabindex] {
+  display: inline-block;
+  padding: 1px 0;
+}
+
+li.container {
+  position: relative;
+  padding: 2px 0 0 2px;
+}
+
+.codebox {
+  padding-left: 14px;
+}
+
+.expander {
+  position: absolute;
+  -moz-appearance: treetwisty;
+  top: 0;
+  left: 0;
+  width: 14px;
+  height: 14px;
+}
+
+.expander[expanded] {
+  -moz-appearance: treetwistyopen;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+}
--- a/browser/themes/pinstripe/jar.mn
+++ b/browser/themes/pinstripe/jar.mn
@@ -140,16 +140,17 @@ browser.jar:
 * skin/classic/browser/devtools/common.css                  (devtools/common.css)
   skin/classic/browser/devtools/arrows.png                  (devtools/arrows.png)
   skin/classic/browser/devtools/commandline.png             (devtools/commandline.png)
   skin/classic/browser/devtools/alerticon-warning.png       (devtools/alerticon-warning.png)
   skin/classic/browser/devtools/goto-mdn.png                (devtools/goto-mdn.png)
   skin/classic/browser/devtools/csshtmltree.css             (devtools/csshtmltree.css)
   skin/classic/browser/devtools/gcli.css                    (devtools/gcli.css)
   skin/classic/browser/devtools/htmlpanel.css               (devtools/htmlpanel.css)
+  skin/classic/browser/devtools/markup-view.css             (devtools/markup-view.css)
   skin/classic/browser/devtools/orion.css                   (devtools/orion.css)
   skin/classic/browser/devtools/orion-container.css         (devtools/orion-container.css)
   skin/classic/browser/devtools/orion-task.png              (devtools/orion-task.png)
   skin/classic/browser/devtools/orion-breakpoint.png        (devtools/orion-breakpoint.png)
   skin/classic/browser/devtools/orion-debug-location.png    (devtools/orion-debug-location.png)
   skin/classic/browser/devtools/toolbarbutton-close.png     (devtools/toolbarbutton-close.png)
 * skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
   skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
new file mode 100644
--- /dev/null
+++ b/browser/themes/winstripe/devtools/markup-view.css
@@ -0,0 +1,71 @@
+/* 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/. */
+
+* {
+  padding: 0;
+  margin: 0;
+}
+
+body {
+  font: message-box;
+  background-color: #131c26;
+  color: #8fa1b2;
+}
+
+.tagname {
+  color: #a673bf;
+}
+
+.attrname {
+  color: #b26b47;
+}
+
+.attrvalue {
+  color: #3689b2;
+}
+
+.newattr {
+  cursor: pointer;
+}
+
+.comment {
+  color: #5c6773;
+}
+
+.selected {
+  background-color: #253847;
+}
+
+/* Give some padding to focusable elements to match the editor input
+ * that will replace them. */
+span[tabindex] {
+  display: inline-block;
+  padding: 1px 0;
+}
+
+li.container {
+  position: relative;
+  padding: 2px 0 0 2px;
+}
+
+.codebox {
+  padding-left: 14px;
+}
+
+.expander {
+  position: absolute;
+  -moz-appearance: treetwisty;
+  top: 0;
+  left: 0;
+  width: 14px;
+  height: 14px;
+}
+
+.expander[expanded] {
+  -moz-appearance: treetwistyopen;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+}
--- a/browser/themes/winstripe/jar.mn
+++ b/browser/themes/winstripe/jar.mn
@@ -128,16 +128,17 @@ browser.jar:
         skin/classic/browser/devtools/common.css                    (devtools/common.css)
         skin/classic/browser/devtools/arrows.png                    (devtools/arrows.png)
         skin/classic/browser/devtools/commandline.png               (devtools/commandline.png)
         skin/classic/browser/devtools/alerticon-warning.png         (devtools/alerticon-warning.png)
         skin/classic/browser/devtools/goto-mdn.png                  (devtools/goto-mdn.png)
         skin/classic/browser/devtools/csshtmltree.css               (devtools/csshtmltree.css)
         skin/classic/browser/devtools/gcli.css                      (devtools/gcli.css)
         skin/classic/browser/devtools/htmlpanel.css                 (devtools/htmlpanel.css)
+        skin/classic/browser/devtools/markup-view.css               (devtools/markup-view.css)
         skin/classic/browser/devtools/orion.css                     (devtools/orion.css)
         skin/classic/browser/devtools/orion-container.css           (devtools/orion-container.css)
         skin/classic/browser/devtools/orion-task.png                (devtools/orion-task.png)
         skin/classic/browser/devtools/orion-breakpoint.png          (devtools/orion-breakpoint.png)
         skin/classic/browser/devtools/orion-debug-location.png      (devtools/orion-debug-location.png)
         skin/classic/browser/devtools/toolbarbutton-close.png       (devtools/toolbarbutton-close.png)
         skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
         skin/classic/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)
@@ -330,16 +331,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/common.css                (devtools/common.css)
         skin/classic/aero/browser/devtools/arrows.png                (devtools/arrows.png)
         skin/classic/aero/browser/devtools/commandline.png           (devtools/commandline.png)
         skin/classic/aero/browser/devtools/alerticon-warning.png     (devtools/alerticon-warning.png)
         skin/classic/aero/browser/devtools/goto-mdn.png              (devtools/goto-mdn.png)
         skin/classic/aero/browser/devtools/csshtmltree.css           (devtools/csshtmltree.css)
         skin/classic/aero/browser/devtools/gcli.css                  (devtools/gcli.css)
         skin/classic/aero/browser/devtools/htmlpanel.css             (devtools/htmlpanel.css)
+        skin/classic/aero/browser/devtools/markup-view.css           (devtools/markup-view.css)
         skin/classic/aero/browser/devtools/orion.css                 (devtools/orion.css)
         skin/classic/aero/browser/devtools/orion-container.css       (devtools/orion-container.css)
         skin/classic/aero/browser/devtools/orion-task.png            (devtools/orion-task.png)
         skin/classic/aero/browser/devtools/orion-breakpoint.png      (devtools/orion-breakpoint.png)
         skin/classic/aero/browser/devtools/orion-debug-location.png  (devtools/orion-debug-location.png)
         skin/classic/aero/browser/devtools/toolbarbutton-close.png   (devtools/toolbarbutton-close.png)
         skin/classic/aero/browser/devtools/webconsole.css                  (devtools/webconsole.css)
         skin/classic/aero/browser/devtools/webconsole_networkpanel.css     (devtools/webconsole_networkpanel.css)