Bug 895561 - 'Edit As HTML' option in the markup view - browser changes, r=jwalker
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 24 Oct 2013 08:41:03 -0500
changeset 165912 8693c78697ae83fb6302aada797e96c537f18699
parent 165911 9c44473817b5ad6555e0e08794e131880a011f8b
child 165913 08f5ba64a98854444bb9aabd1b3952149130861b
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs895561
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 895561 - 'Edit As HTML' option in the markup view - browser changes, r=jwalker
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/inspector.xul
browser/devtools/markupview/html-editor.js
browser/devtools/markupview/markup-view.css
browser/devtools/markupview/markup-view.js
browser/devtools/markupview/markup-view.xhtml
browser/devtools/markupview/test/browser.ini
browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js
browser/devtools/markupview/test/head.js
browser/devtools/sourceeditor/codemirror/mozilla.css
browser/devtools/sourceeditor/editor.js
browser/locales/en-US/chrome/browser/devtools/inspector.dtd
browser/themes/shared/devtools/dark-theme.css
browser/themes/shared/devtools/light-theme.css
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -81,16 +81,18 @@ InspectorPanel.prototype = {
     }).then(defaultSelection => {
       return this._deferredOpen(defaultSelection);
     }).then(null, console.error);
   },
 
   _deferredOpen: function(defaultSelection) {
     let deferred = promise.defer();
 
+    this.outerHTMLEditable = this._target.client.traits.editOuterHTML;
+
     this.onNewRoot = this.onNewRoot.bind(this);
     this.walker.on("new-root", this.onNewRoot);
 
     this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
     this.lastNodemenuItem = this.nodemenu.lastChild;
     this._setupNodeMenu = this._setupNodeMenu.bind(this);
     this._resetNodeMenu = this._resetNodeMenu.bind(this);
     this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
@@ -588,25 +590,33 @@ InspectorPanel.prototype = {
       deleteNode.removeAttribute("disabled");
     }
 
     // Disable / enable "Copy Unique Selector", "Copy inner HTML" &
     // "Copy outer HTML" as appropriate
     let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
     let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
     let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
-    if (this.selection.isElementNode()) {
+    let selectionIsElement = this.selection.isElementNode();
+    if (selectionIsElement) {
       unique.removeAttribute("disabled");
       copyInnerHTML.removeAttribute("disabled");
       copyOuterHTML.removeAttribute("disabled");
     } else {
       unique.setAttribute("disabled", "true");
       copyInnerHTML.setAttribute("disabled", "true");
       copyOuterHTML.setAttribute("disabled", "true");
     }
+
+    let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
+    if (this.outerHTMLEditable && selectionIsElement) {
+      editHTML.removeAttribute("disabled");
+    } else {
+      editHTML.setAttribute("disabled", "true");
+    }
   },
 
   _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
     // Remove any extra items
     while (this.lastNodemenuItem.nextSibling) {
       let toDelete = this.lastNodemenuItem.nextSibling;
       toDelete.parentNode.removeChild(toDelete);
     }
@@ -701,16 +711,29 @@ InspectorPanel.prototype = {
       this.highlighter.hide();
     }
     else if (event.type == "mouseout") {
       this.highlighter.show();
     }
   },
 
   /**
+   * Edit the outerHTML of the selected Node.
+   */
+  editHTML: function InspectorPanel_editHTML()
+  {
+    if (!this.selection.isNode()) {
+      return;
+    }
+    if (this.markup) {
+      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
+    }
+  },
+
+  /**
    * Copy the innerHTML of the selected Node to the clipboard.
    */
   copyInnerHTML: function InspectorPanel_copyInnerHTML()
   {
     if (!this.selection.isNode()) {
       return;
     }
     this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -28,16 +28,20 @@
       key="&inspectorSearchHTML.key;"
       modifiers="accel"
       command="nodeSearchCommand"/>
   </keyset>
 
   <popupset id="inspectorPopupSet">
     <!-- Used by the Markup Panel, the Highlighter and the Breadcrumbs -->
     <menupopup id="inspector-node-popup">
+      <menuitem id="node-menu-edithtml"
+        label="&inspectorHTMLEdit.label;"
+        accesskey="&inspectorHTMLEdit.accesskey;"
+        oncommand="inspector.editHTML()"/>
       <menuitem id="node-menu-copyinner"
         label="&inspectorHTMLCopyInner.label;"
         accesskey="&inspectorHTMLCopyInner.accesskey;"
         oncommand="inspector.copyInnerHTML()"/>
       <menuitem id="node-menu-copyouter"
         label="&inspectorHTMLCopyOuter.label;"
         accesskey="&inspectorHTMLCopyOuter.accesskey;"
         oncommand="inspector.copyOuterHTML()"/>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/html-editor.js
@@ -0,0 +1,182 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ "use strict";
+
+const {Cu} = require("chrome");
+const Editor = require("devtools/sourceeditor/editor");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+exports.HTMLEditor = HTMLEditor;
+
+function ctrl(k) {
+  return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
+}
+function stopPropagation(e) {
+  e.stopPropagation();
+}
+/**
+ * A wrapper around the Editor component, that allows editing of HTML.
+ *
+ * The main functionality this provides around the Editor is the ability
+ * to show/hide/position an editor inplace. It only appends once to the
+ * body, and uses CSS to position the editor.  The reason it is done this
+ * way is that the editor is loaded in an iframe, and calling appendChild
+ * causes it to reload.
+ *
+ * Meant to be embedded inside of an HTML page, as in markup-view.xhtml.
+ *
+ * @param HTMLDocument htmlDocument
+ *        The document to attach the editor to.  Will also use this
+ *        document as a basis for listening resize events.
+ */
+function HTMLEditor(htmlDocument)
+{
+  this.doc = htmlDocument;
+  this.container = this.doc.createElement("div");
+  this.container.className = "html-editor theme-body";
+  this.container.style.display = "none";
+  this.editorInner = this.doc.createElement("div");
+  this.editorInner.className = "html-editor-inner";
+  this.container.appendChild(this.editorInner);
+
+  this.doc.body.appendChild(this.container);
+  this.hide = this.hide.bind(this);
+  this.refresh = this.refresh.bind(this);
+
+  EventEmitter.decorate(this);
+
+  this.doc.defaultView.addEventListener("resize",
+    this.refresh, true);
+
+  let config = {
+    mode: Editor.modes.html,
+    lineWrapping: true,
+    styleActiveLine: false,
+    extraKeys: {},
+    theme: "mozilla markup-view"
+  };
+
+  config.extraKeys[ctrl("Enter")] = this.hide;
+  config.extraKeys["Esc"] = this.hide.bind(this, false);
+
+  this.container.addEventListener("click", this.hide, false);
+  this.editorInner.addEventListener("click", stopPropagation, false);
+  this.editor = new Editor(config);
+
+  this.editor.appendTo(this.editorInner).then(() => {
+    this.hide(false);
+  }).then(null, (err) => console.log(err.message));
+}
+
+HTMLEditor.prototype = {
+
+  /**
+   * Need to refresh position by manually setting CSS values, so this will
+   * need to be called on resizes and other sizing changes.
+   */
+  refresh: function() {
+    let element = this._attachedElement;
+
+    if (element) {
+      this.container.style.top = element.offsetTop + "px";
+      this.container.style.left = element.offsetLeft + "px";
+      this.container.style.width = element.offsetWidth + "px";
+      this.container.style.height = element.parentNode.offsetHeight + "px";
+      this.editor.refresh();
+    }
+  },
+
+  /**
+   * Anchor the editor to a particular element.
+   *
+   * @param DOMNode element
+   *        The element that the editor will be anchored to.
+   *        Should belong to the HTMLDocument passed into the constructor.
+   */
+  _attach: function(element)
+  {
+    this._detach();
+    this._attachedElement = element;
+    element.classList.add("html-editor-container");
+    this.refresh();
+  },
+
+  /**
+   * Unanchor the editor from an element.
+   */
+  _detach: function()
+  {
+    if (this._attachedElement) {
+      this._attachedElement.classList.remove("html-editor-container");
+      this._attachedElement = undefined;
+    }
+  },
+
+  /**
+   * Anchor the editor to a particular element, and show the editor.
+   *
+   * @param DOMNode element
+   *        The element that the editor will be anchored to.
+   *        Should belong to the HTMLDocument passed into the constructor.
+   * @param string text
+   *        Value to set the contents of the editor to
+   * @param function cb
+   *        The function to call when hiding
+   */
+  show: function(element, text)
+  {
+    if (this._visible) {
+      return;
+    }
+
+    this._originalValue = text;
+    this.editor.setText(text);
+    this._attach(element);
+    this.container.style.display = "flex";
+    this._visible = true;
+
+    this.editor.refresh();
+    this.editor.focus();
+  },
+
+  /**
+   * Hide the editor, optionally committing the changes
+   *
+   * @param bool shouldCommit
+   *             A change will be committed by default.  If this param
+   *             strictly equals false, no change will occur.
+   */
+  hide: function(shouldCommit)
+  {
+    if (!this._visible) {
+      return;
+    }
+
+    this.container.style.display = "none";
+    this._detach();
+
+    let newValue = this.editor.getText();
+    let valueHasChanged = this._originalValue !== newValue;
+    let preventCommit = shouldCommit === false || !valueHasChanged;
+    this.emit("popup-hidden", !preventCommit, newValue);
+    this._originalValue = undefined;
+    this._visible = undefined;
+  },
+
+  /**
+   * Destroy this object and unbind all event handlers
+   */
+  destroy: function()
+  {
+    this.doc.defaultView.removeEventListener("resize",
+      this.refresh, true);
+    this.container.removeEventListener("click", this.hide, false);
+    this.editorInner.removeEventListener("click", stopPropagation, false);
+
+    this.hide(false);
+    this.container.parentNode.removeChild(this.container);
+  }
+};
\ No newline at end of file
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -9,16 +9,42 @@
   float: left;
   min-width: 100%;
 }
 
 #root-wrapper:after {
    content: "";
    display: block;
    clear: both;
+   position:relative;
+}
+
+.html-editor {
+  display: none;
+  position: absolute;
+  z-index: 2;
+
+  /* Use the same margin/padding trick used by .child tags to ensure that
+   * the editor covers up any content to the left (including expander arrows
+   * and hover effects). */
+  margin-left: -1000em;
+  padding-left: 1000em;
+}
+
+.html-editor-inner {
+  border: solid .1px;
+  flex: 1 1 auto;
+}
+
+.html-editor iframe {
+  height: 100%;
+  width: 100%;
+  border: none;
+  margin: 0;
+  padding: 0;
 }
 
 .children {
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
@@ -31,16 +57,21 @@
 }
 
 .tag-line {
   min-height: 1.4em;
   line-height: 1.4em;
   position: relative;
 }
 
+.html-editor-container {
+  position: relative;
+  min-height: 200px;
+}
+
 /* This extra element placed in each tag is positioned absolutely to cover the
  * whole tag line and is used for background styling (when a selection is made
  * or when the tag is flashing) */
 .tag-line .highlighter {
   position: absolute;
   left: -1000em;
   right: 0;
   height: 100%;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -13,16 +13,17 @@ const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 const CONTAINER_FLASHING_DURATION = 500;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {HTMLEditor} = require("devtools/markupview/html-editor");
 const {OutputParser} = require("devtools/output-parser");
 const promise = require("sdk/core/promise");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
@@ -52,16 +53,17 @@ loader.lazyGetter(this, "AutocompletePop
  */
 function MarkupView(aInspector, aFrame, aControllerWindow) {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
   this._outputParser = new OutputParser();
+  this.htmlEditor = new HTMLEditor(this.doc);
 
   this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
@@ -144,16 +146,17 @@ MarkupView.prototype = {
     // Recursively update each node starting with documentElement.
     updateChildren(documentElement);
   },
 
   /**
    * Highlight the inspector selected node.
    */
   _onNewSelection: function() {
+    this.htmlEditor.hide();
     let done = this._inspector.updating("markup-view");
     if (this._inspector.selection.isNode()) {
       this.showNode(this._inspector.selection.nodeFront, true).then(() => {
         this.markNodeAsSelected(this._inspector.selection.nodeFront);
         done();
       });
     } else {
       this.unmarkSelectedNode();
@@ -331,18 +334,18 @@ MarkupView.prototype = {
    *        If falsy, keyboard focus will be moved to the container too.
    */
   navigate: function(aContainer, aIgnoreFocus) {
     if (!aContainer) {
       return;
     }
 
     let node = aContainer.node;
-    this.markNodeAsSelected(node);
-    this._inspector.selection.setNodeFront(node, "treepanel");
+    this.markNodeAsSelected(node, "treepanel");
+
     // This event won't be fired if the node is the same. But the highlighter
     // need to lock the node if it wasn't.
     this._inspector.selection.emit("new-node");
     this._inspector.selection.emit("new-node-front");
 
     if (!aIgnoreFocus) {
       aContainer.focus();
     }
@@ -385,16 +388,19 @@ MarkupView.prototype = {
     return container;
   },
 
   /**
    * Mutation observer used for included nodes.
    */
   _mutationObserver: function(aMutations) {
     let requiresLayoutChange = false;
+    let reselectParent;
+    let reselectChildIndex;
+
     for (let mutation of aMutations) {
       let type = mutation.type;
       let target = mutation.target;
 
       if (mutation.type === "documentUnload") {
         // Treat this as a childList change of the child (maybe the protocol
         // should do this).
         type = "childList";
@@ -413,30 +419,61 @@ MarkupView.prototype = {
       if (type === "attributes" || type === "characterData") {
         container.update();
 
         // Auto refresh style properties on selected node when they change.
         if (type === "attributes" && container.selected) {
           requiresLayoutChange = true;
         }
       } else if (type === "childList") {
+        let isFromOuterHTML = mutation.removed.some((n) => {
+          return n === this._outerHTMLNode;
+        });
+
+        // Keep track of which node should be reselected after mutations.
+        if (isFromOuterHTML) {
+          reselectParent = target;
+          reselectChildIndex = this._outerHTMLChildIndex;
+
+          delete this._outerHTMLNode;
+          delete this._outerHTMLChildIndex;
+        }
+
         container.childrenDirty = true;
-        // Update the children to take care of changes in the DOM
-        // Passing true as the last parameter asks for mutation flashing of the
-        // new nodes
-        this._updateChildren(container, {flash: true});
+        // Update the children to take care of changes in the markup view DOM.
+        this._updateChildren(container, {flash: !isFromOuterHTML});
       }
     }
 
     if (requiresLayoutChange) {
       this._inspector.immediateLayoutChange();
     }
-    this._waitForChildren().then(() => {
+    this._waitForChildren().then((nodes) => {
       this._flashMutatedNodes(aMutations);
-      this._inspector.emit("markupmutation");
+      this._inspector.emit("markupmutation", aMutations);
+
+      // Since the htmlEditor is absolutely positioned, a mutation may change
+      // the location in which it should be shown.
+      this.htmlEditor.refresh();
+
+      // If a node has had its outerHTML set, the parent node will be selected.
+      // Reselect the original node immediately.
+      if (this._inspector.selection.nodeFront === reselectParent) {
+        this.walker.children(reselectParent).then((o) => {
+          let node = o.nodes[reselectChildIndex];
+          let container = this._containers.get(node);
+          if (node && container) {
+            this.markNodeAsSelected(node, "outerhtml");
+            if (container.hasChildren) {
+              this.expandNode(node);
+            }
+          }
+        });
+
+      }
     });
   },
 
   /**
    * Given a list of mutations returned by the mutation observer, flash the
    * corresponding containers to attract attention.
    */
   _flashMutatedNodes: function(aMutations) {
@@ -546,40 +583,131 @@ MarkupView.prototype = {
   /**
    * Collapse the node's children.
    */
   collapseNode: function(aNode) {
     let container = this._containers.get(aNode);
     container.expanded = false;
   },
 
+  /**
+   * Retrieve the outerHTML for a remote node.
+   * @param aNode The NodeFront to get the outerHTML for.
+   * @returns A promise that will be resolved with the outerHTML.
+   */
+  getNodeOuterHTML: function(aNode) {
+    let def = promise.defer();
+    this.walker.outerHTML(aNode).then(longstr => {
+      longstr.string().then(outerHTML => {
+        longstr.release().then(null, console.error);
+        def.resolve(outerHTML);
+      });
+    });
+    return def.promise;
+  },
+
+  /**
+   * Retrieve the index of a child within its parent's children list.
+   * @param aNode The NodeFront to find the index of.
+   * @returns A promise that will be resolved with the integer index.
+   *          If the child cannot be found, returns -1
+   */
+  getNodeChildIndex: function(aNode) {
+    let def = promise.defer();
+    let parentNode = aNode.parentNode();
+
+    // Node may have been removed from the DOM, instead of throwing an error,
+    // return -1 indicating that it isn't inside of its parent children list.
+    if (!parentNode) {
+      def.resolve(-1);
+    } else {
+      this.walker.children(parentNode).then(children => {
+        def.resolve(children.nodes.indexOf(aNode));
+      });
+    }
+
+    return def.promise;
+  },
+
+  /**
+   * Retrieve the index of a child within its parent's children collection.
+   * @param aNode The NodeFront to find the index of.
+   * @param newValue The new outerHTML to set on the node.
+   * @param oldValue The old outerHTML that will be reverted to find the index of.
+   * @returns A promise that will be resolved with the integer index.
+   *          If the child cannot be found, returns -1
+   */
+  updateNodeOuterHTML: function(aNode, newValue, oldValue) {
+    let container = this._containers.get(aNode);
+    if (!container) {
+      return;
+    }
+
+    this.getNodeChildIndex(aNode).then((i) => {
+      this._outerHTMLChildIndex = i;
+      this._outerHTMLNode = aNode;
+
+      container.undo.do(() => {
+        this.walker.setOuterHTML(aNode, newValue);
+      }, () => {
+        this.walker.setOuterHTML(aNode, oldValue);
+      });
+    });
+  },
+
+  /**
+   * Open an editor in the UI to allow editing of a node's outerHTML.
+   * @param aNode The NodeFront to edit.
+   */
+  beginEditingOuterHTML: function(aNode) {
+    this.getNodeOuterHTML(aNode).then((oldValue)=> {
+      let container = this._containers.get(aNode);
+      if (!container) {
+        return;
+      }
+      this.htmlEditor.show(container.tagLine, oldValue);
+      this.htmlEditor.once("popup-hidden", (e, aCommit, aValue) => {
+        if (aCommit) {
+          this.updateNodeOuterHTML(aNode, aValue, oldValue);
+        }
+      });
+    });
+  },
+
+  /**
+   * Mark the given node expanded.
+   * @param aNode The NodeFront to mark as expanded.
+   */
   setNodeExpanded: function(aNode, aExpanded) {
     if (aExpanded) {
       this.expandNode(aNode);
     } else {
       this.collapseNode(aNode);
     }
   },
 
   /**
-   * Mark the given node selected.
+   * Mark the given node selected, and update the inspector.selection
+   * object's NodeFront to keep consistent state between UI and selection.
+   * @param aNode The NodeFront to mark as selected.
    */
-  markNodeAsSelected: function(aNode) {
+  markNodeAsSelected: function(aNode, reason) {
     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._inspector.selection.setNodeFront(aNode, reason || "nodeselected");
     return true;
   },
 
   /**
    * Make sure that every ancestor of the selection are updated
    * and included in the list of visible children.
    */
   _ensureVisible: function(node) {
@@ -774,16 +902,19 @@ MarkupView.prototype = {
   },
 
   /**
    * Tear down the markup panel.
    */
   destroy: function() {
     gDevTools.off("pref-changed", this._handlePrefChange);
 
+    this.htmlEditor.destroy();
+    delete this.htmlEditor;
+
     this.undo.destroy();
     delete this.undo;
 
     this.popup.destroy();
     delete this.popup;
 
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -6,17 +6,17 @@
 
 <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"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
 
-  <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+  <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"></script>
 
 </head>
 <body class="theme-body devtools-monospace" role="application">
   <div id="root-wrapper">
     <div id="root"></div>
   </div>
   <div id="templates" style="display:none">
 
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 support-files = head.js
 
 [browser_bug896181_css_mixed_completion_new_attribute.js]
 # Bug 916763 - too many intermittent failures
 skip-if = true
 [browser_inspector_markup_edit.html]
 [browser_inspector_markup_edit.js]
+[browser_inspector_markup_edit_outerhtml.js]
 [browser_inspector_markup_mutation.html]
 [browser_inspector_markup_mutation.js]
 [browser_inspector_markup_mutation_flashing.html]
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.html]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.html]
 [browser_inspector_markup_subset.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js
@@ -0,0 +1,295 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+  let inspector;
+  let doc;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+
+  let outerHTMLs = [
+    {
+      selector: "#one",
+      oldHTML: '<div id="one">First <em>Div</em></div>',
+      newHTML: '<div id="one">First Div</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode.textContent, "First Div", "New div has expected text content");
+        ok (!doc.querySelector("#one em"), "No em remaining")
+      }
+    },
+    {
+      selector: "#removedChildren",
+      oldHTML: '<div id="removedChildren">removedChild <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>',
+      newHTML: '<div id="removedChildren">removedChild</div>'
+    },
+    {
+      selector: "#addedChildren",
+      oldHTML: '<div id="addedChildren">addedChildren</div>',
+      newHTML: '<div id="addedChildren">addedChildren <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>'
+    },
+    {
+      selector: "#addedAttribute",
+      oldHTML: '<div id="addedAttribute">addedAttribute</div>',
+      newHTML: '<div id="addedAttribute" class="important" disabled checked>addedAttribute</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+        is (pageNode.outerHTML, '<div id="addedAttribute" class="important" disabled="" checked="">addedAttribute</div>',
+              "Attributes have been added");
+      }
+    },
+    {
+      selector: "#changedTag",
+      oldHTML: '<div id="changedTag">changedTag</div>',
+      newHTML: '<p id="changedTag" class="important">changedTag</p>'
+    },
+    {
+      selector: "#badMarkup1",
+      oldHTML: '<div id="badMarkup1">badMarkup1</div>',
+      newHTML: '<div id="badMarkup1">badMarkup1</div> hanging</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let textNode = pageNode.nextSibling;
+
+        is (textNode.nodeName, "#text", "Sibling is a text element");
+        is (textNode.data, " hanging", "New text node has expected text content");
+      }
+    },
+    {
+      selector: "#badMarkup2",
+      oldHTML: '<div id="badMarkup2">badMarkup2</div>',
+      newHTML: '<div id="badMarkup2">badMarkup2</div> hanging<div></div></div></div></body>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let textNode = pageNode.nextSibling;
+
+        is (textNode.nodeName, "#text", "Sibling is a text element");
+        is (textNode.data, " hanging", "New text node has expected text content");
+      }
+    },
+    {
+      selector: "#badMarkup3",
+      oldHTML: '<div id="badMarkup3">badMarkup3</div>',
+      newHTML: '<div id="badMarkup3">badMarkup3 <em>Emphasized <strong> and strong</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let em = doc.querySelector("#badMarkup3 em");
+        let strong = doc.querySelector("#badMarkup3 strong");
+
+        is (em.textContent, "Emphasized  and strong", "<em> was auto created");
+        is (strong.textContent, " and strong", "<strong> was auto created");
+      }
+    },
+    {
+      selector: "#badMarkup4",
+      oldHTML: '<div id="badMarkup4">badMarkup4</div>',
+      newHTML: '<div id="badMarkup4">badMarkup4</p>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let div = doc.querySelector("#badMarkup4");
+        let p = doc.querySelector("#badMarkup4 p");
+
+        is (div.textContent, "badMarkup4", "textContent is correct");
+        is (div.tagName, "DIV", "did not change to <p> tag");
+        is (p.textContent, "", "The <p> tag has no children");
+        is (p.tagName, "P", "Created an empty <p> tag");
+      }
+    },
+    {
+      selector: "#badMarkup5",
+      oldHTML: '<p id="badMarkup5">badMarkup5</p>',
+      newHTML: '<p id="badMarkup5">badMarkup5 <div>with a nested div</div></p>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let p = doc.querySelector("#badMarkup5");
+        let nodiv = doc.querySelector("#badMarkup5 div");
+        let div = doc.querySelector("#badMarkup5 ~ div");
+
+        ok (!nodiv, "The invalid markup got created as a sibling");
+        is (p.textContent, "badMarkup5 ", "The <p> tag does not take in the <div> content");
+        is (p.tagName, "P", "Did not change to a <div> tag");
+        is (div.textContent, "with a nested div", "textContent is correct");
+        is (div.tagName, "DIV", "Did not change to <p> tag");
+      }
+    },
+    {
+      selector: "#siblings",
+      oldHTML: '<div id="siblings">siblings</div>',
+      newHTML: '<div id="siblings-before-sibling">before sibling</div>' +
+               '<div id="siblings">siblings (updated)</div>' +
+               '<div id="siblings-after-sibling">after sibling</div>',
+      validate: function(pageNode, selectedNode) {
+        let beforeSiblingNode = doc.querySelector("#siblings-before-sibling");
+        let afterSiblingNode = doc.querySelector("#siblings-after-sibling");
+
+        is (beforeSiblingNode, selectedNode, "Sibling has been selected");
+        is (pageNode.textContent, "siblings (updated)", "New div has expected text content");
+        is (beforeSiblingNode.textContent, "before sibling", "Sibling has been inserted");
+        is (afterSiblingNode.textContent, "after sibling", "Sibling has been inserted");
+      }
+    }
+  ];
+  content.location = "data:text/html," +
+    "<!DOCTYPE html>" +
+    "<head><meta charset='utf-8' /></head>" +
+    "<body>" +
+    [outer.oldHTML for (outer of outerHTMLs) ].join("\n") +
+    "</body>" +
+    "</html>";
+
+  function setupTest() {
+    var target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+      inspector = toolbox.getCurrentPanel();
+      inspector.once("inspector-updated", startTests);
+    });
+  }
+
+  function startTests() {
+    inspector.markup._frame.focus();
+    nextStep(0);
+  }
+
+  function nextStep(cursor) {
+    if (cursor >= outerHTMLs.length) {
+      testBody();
+      return;
+    }
+
+    let currentTestData = outerHTMLs[cursor];
+    let selector = currentTestData.selector;
+    let oldHTML = currentTestData.oldHTML;
+    let newHTML = currentTestData.newHTML;
+    let rawNode = doc.querySelector(selector);
+
+    inspector.selection.once("new-node", () => {
+
+      let oldNodeFront = inspector.selection.nodeFront;
+
+      // markupmutation fires once the outerHTML is set, with a target
+      // as the parent node and a type of "childList".
+      inspector.once("markupmutation", (e, aMutations) => {
+
+        // Check to make the sure the correct mutation has fired, and that the
+        // parent is selected (this will be reset to the child once the mutation is complete.
+        let node = inspector.selection.node;
+        let nodeFront = inspector.selection.nodeFront;
+        let mutation = aMutations[0];
+        let isFromOuterHTML = mutation.removed.some((n) => {
+          return n === oldNodeFront;
+        });
+
+        ok (isFromOuterHTML, "The node is in the 'removed' list of the mutation");
+        is (mutation.type, "childList", "Mutation is a childList after updating outerHTML");
+        is (mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML");
+
+        // Wait for node to be reselected after outerHTML has been set
+        inspector.selection.once("new-node", () => {
+
+          // Typically selectedNode will === pageNode, but if a new element has been injected in front
+          // of it, this will not be the case.  If this happens.
+          let selectedNode = inspector.selection.node;
+          let nodeFront = inspector.selection.nodeFront;
+          let pageNode = doc.querySelector(selector);
+
+          if (currentTestData.validate) {
+            currentTestData.validate(pageNode, selectedNode);
+          } else {
+            is (pageNode, selectedNode, "Original node (grabbed by selector) is selected");
+            is (pageNode.outerHTML, newHTML, "Outer HTML has been updated");
+          }
+
+          nextStep(cursor + 1);
+        });
+
+      });
+
+      is (inspector.selection.node, rawNode, "Selection is on the correct node");
+      inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, newHTML, oldHTML);
+    });
+
+    inspector.selection.setNode(rawNode);
+  }
+
+  function testBody() {
+    let body = doc.querySelector("body");
+    let bodyHTML = '<body id="updated"><p></p></body>';
+    let bodyFront = inspector.markup.walker.frontForRawNode(body);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.querySelector("body").outerHTML, bodyHTML, "<body> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      testHead();
+    });
+    inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
+  }
+
+  function testHead() {
+    let head = doc.querySelector("head");
+    let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
+    let headFront = inspector.markup.walker.frontForRawNode(head);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "New Title", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.querySelector("head").outerHTML, headHTML, "<head> HTML has been updated");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      testDocumentElement();
+    });
+    inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
+  }
+
+  function testDocumentElement() {
+    let docElement = doc.documentElement;
+    let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
+    let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "Updated from document element", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.documentElement.id, "updated", "<html> ID has been updated");
+      is (doc.documentElement.className, "", "<html> class has been updated");
+      is (doc.documentElement.getAttribute("foo"), "bar", "<html> attribute has been updated");
+      is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      is (doc.body.textContent, "Hello", "document.body.textContent has been updated");
+      testDocumentElement2();
+    });
+    inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
+  }
+
+  function testDocumentElement2() {
+    let docElement = doc.documentElement;
+    let docElementHTML = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
+    let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "Updated again from document element", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.documentElement.id, "somethingelse", "<html> ID has been updated");
+      is (doc.documentElement.className, "updated", "<html> class has been updated");
+      is (doc.documentElement.getAttribute("foo"), null, "<html> attribute has been removed");
+      is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      is (doc.body.textContent, "Hello again", "document.body.textContent has been updated");
+      finishUp();
+    });
+    inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
+  }
+
+  function finishUp() {
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -1,16 +1,17 @@
 /* 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;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let TargetFactory = devtools.TargetFactory;
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 
 // 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");
 }
 
--- a/browser/devtools/sourceeditor/codemirror/mozilla.css
+++ b/browser/devtools/sourceeditor/codemirror/mozilla.css
@@ -18,13 +18,9 @@
 
 .debugLocation {
   background-image: url("chrome://browser/skin/devtools/orion-debug-location.png");
 }
 
 .breakpoint.debugLocation {
   background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"),
     url("chrome://browser/skin/devtools/orion-breakpoint.png");
-}
-
-.CodeMirror-activeline-background {
-  background: #e8f2ff;
 }
\ No newline at end of file
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -22,22 +22,24 @@ const events  = require("devtools/shared
 Cu.import("resource://gre/modules/Services.jsm");
 const L10N = Services.strings.createBundle(L10N_BUNDLE);
 
 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
 // JavaScript and CSS that is injected into an iframe in
 // order to initialize a CodeMirror instance.
 
 const CM_STYLES   = [
+  "chrome://browser/skin/devtools/common.css",
   "chrome://browser/content/devtools/codemirror/codemirror.css",
   "chrome://browser/content/devtools/codemirror/dialog.css",
   "chrome://browser/content/devtools/codemirror/mozilla.css"
 ];
 
 const CM_SCRIPTS  = [
+  "chrome://browser/content/devtools/theme-switching.js",
   "chrome://browser/content/devtools/codemirror/codemirror.js",
   "chrome://browser/content/devtools/codemirror/dialog.js",
   "chrome://browser/content/devtools/codemirror/searchcursor.js",
   "chrome://browser/content/devtools/codemirror/search.js",
   "chrome://browser/content/devtools/codemirror/matchbrackets.js",
   "chrome://browser/content/devtools/codemirror/closebrackets.js",
   "chrome://browser/content/devtools/codemirror/comment.js",
   "chrome://browser/content/devtools/codemirror/javascript.js",
@@ -53,33 +55,34 @@ const CM_IFRAME   =
   "  <head>" +
   "    <style>" +
   "      html, body { height: 100%; }" +
   "      body { margin: 0; overflow: hidden; }" +
   "      .CodeMirror { width: 100%; height: 100% !important; }" +
   "    </style>" +
 [ "    <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
   "  </head>" +
-  "  <body></body>" +
+  "  <body class='theme-body devtools-monospace'></body>" +
   "</html>";
 
 const CM_MAPPING = [
   "focus",
   "hasFocus",
   "getCursor",
   "somethingSelected",
   "setSelection",
   "getSelection",
   "replaceSelection",
   "undo",
   "redo",
   "clearHistory",
   "openDialog",
   "cursorCoords",
-  "lineCount"
+  "lineCount",
+  "refresh"
 ];
 
 const CM_JUMP_DIALOG = [
   L10N.GetStringFromName("gotoLineCmd.promptTitle")
     + " <input type=text style='width: 10em'/>"
 ];
 
 const { cssProperties, cssValues, cssColors } = getCSSKeywords();
@@ -127,17 +130,18 @@ function Editor(config) {
     value:           "",
     mode:            Editor.modes.text,
     indentUnit:      tabSize,
     tabSize:         tabSize,
     contextMenu:     null,
     matchBrackets:   true,
     extraKeys:       {},
     indentWithTabs:  useTabs,
-    styleActiveLine: true
+    styleActiveLine: true,
+    theme: "mozilla"
   };
 
   // Overwrite default config with user-provided, if needed.
   Object.keys(config).forEach((k) => this.config[k] = config[k]);
 
   // Additional shortcuts.
   this.config.extraKeys[ctrl("J")] = (cm) => this.jumpToLine();
   this.config.extraKeys[ctrl("/")] = "toggleComment";
@@ -177,17 +181,17 @@ Editor.prototype = {
    * CodeMirror and all its dependencies.
    *
    * This method is asynchronous and returns a promise.
    */
   appendTo: function (el) {
     let def = promise.defer();
     let cm  = editors.get(this);
     let doc = el.ownerDocument;
-    let env = doc.createElementNS(XUL_NS, "iframe");
+    let env = doc.createElement("iframe");
     env.flex = 1;
 
     if (cm)
       throw new Error("You can append an editor only once.");
 
     let onLoad = () => {
       // Once the iframe is loaded, we can inject CodeMirror
       // and its dependencies into its DOM.
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
@@ -1,8 +1,11 @@
+<!ENTITY inspectorHTMLEdit.label       "Edit As HTML">
+<!ENTITY inspectorHTMLEdit.accesskey   "E">
+
 <!ENTITY inspectorHTMLCopyInner.label       "Copy Inner HTML">
 <!ENTITY inspectorHTMLCopyInner.accesskey   "I">
 
 <!ENTITY inspectorHTMLCopyOuter.label       "Copy Outer HTML">
 <!ENTITY inspectorHTMLCopyOuter.accesskey   "O">
 
 <!ENTITY inspectorCopyUniqueSelector.label       "Copy Unique Selector">
 <!ENTITY inspectorCopyUniqueSelector.accesskey   "U">
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -3,17 +3,17 @@
  * 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/. */
 
 /* According to:
  * https://bugzilla.mozilla.org/show_bug.cgi?id=715472#c17
  */
 .theme-body {
   background: #131c26;
-  color: #8fa1b2
+  color: #8fa1b2;
 }
 
 .theme-twisty {
   cursor: pointer;
   width: 14px;
   height: 14px;
   background-repeat: no-repeat;
   background-image: url("chrome://browser/skin/devtools/controls.png");
@@ -42,71 +42,133 @@
 .theme-checkbox[checked] {
   background-position: -42px 0;
 }
 
 .theme-selected {
   background: #26394D;
 }
 
-.theme-bg-darker {
+.theme-bg-darker,
+.cm-s-mozilla .CodeMirror-gutters {
   background-color: rgba(0,0,0,0.5);
 }
 
 .theme-bg-contrast { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
-.theme-link { /* blue */
+.theme-link,
+.cm-s-mozilla .cm-link { /* blue */
   color: #3689b2;
 }
 
-.theme-comment { /* grey */
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr { /* grey */
   color: #5c6773;
 }
 
 .theme-gutter {
   background-color: #0f171f;
   color: #667380;
   border-color: #303b47;
 }
 
 .theme-separator { /* grey */
   border-color: #303b47;
 }
 
-.theme-fg-color1 { /* green */
+.theme-fg-color1,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
   color: #5c9966;
 }
 
-.theme-fg-color2 { /* blue */
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-variable,
+.cm-s-mozilla .cm-def,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-property,
+.cm-s-mozilla .cm-qualifier { /* blue */
   color: #3689b2;
 }
 
-.theme-fg-color3 { /* pink/lavender */
+.theme-fg-color3,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header { /* pink/lavender */
   color: #a673bf;
 }
 
-.theme-fg-color4 { /* purple/violet */
+.theme-fg-color4,
+.cm-s-mozilla .cm-comment { /* purple/violet */
   color: #6270b2;
 }
 
-.theme-fg-color5 { /* Yellow */
+.theme-fg-color5,
+.cm-s-mozilla .cm-bracket,
+.cm-s-mozilla .cm-atom,
+.cm-s-mozilla .cm-keyword { /* Yellow */
   color: #a18650;
 }
 
-.theme-fg-color6 { /* Orange */
+.theme-fg-color6,
+.cm-s-mozilla .cm-string  { /* Orange */
   color: #b26b47;
 }
 
-.theme-fg-color7 { /* Red */
+.theme-fg-color7,
+.cm-s-mozilla .CodeMirror-nonmatchingbracket,
+.cm-s-mozilla .cm-string-2,
+.cm-s-mozilla .cm-error { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
 }
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror { /* Inherit platform specific font sizing and styles */
+  font-family: inherit;
+  font-size: inherit;
+  background: transparent;
+}
+
+.CodeMirror pre,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special,
+.cm-s-mozilla .cm-number { /* theme-body color */
+  color: #8fa1b2;
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+  border-left: solid 1px #fff;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+  background: rgb(185, 215, 253);
+}
+
+.dcm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+  background: rgb(176, 176, 176);
+}
+
+.CodeMirror-activeline-background { /* selected color with alpha */
+  background: rgba(185, 215, 253, .05);
+}
+
+.cm-s-markup-view pre {
+  line-height: 1.4em;
+  min-height: 1.4em;
+}
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -42,71 +42,133 @@
 .theme-checkbox[checked] {
   background-position: -14px 0;
 }
 
 .theme-selected {
   background-color: #CCC;
 }
 
-.theme-bg-darker {
+.theme-bg-darker,
+.cm-s-mozilla .CodeMirror-gutters {
   background: #EFEFEF;
 }
 
 .theme-bg-contrast { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
-.theme-link { /* blue */
+.theme-link,
+.cm-s-mozilla .cm-link { /* blue */
   color: hsl(208,56%,40%);
 }
 
-.theme-comment { /* grey */
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr { /* grey */
   color: hsl(90,2%,46%);
 }
 
 .theme-gutter {
   background-color: hsl(0,0%,90%);
   color: #667380;
   border-color: hsl(0,0%,65%);
 }
 
 .theme-separator { /* grey */
   border-color: #cddae5;
 }
 
-.theme-fg-color1 { /* green */
+.theme-fg-color1,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
   color: hsl(72,100%,27%);
 }
 
-.theme-fg-color2 { /* blue */
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-variable,
+.cm-s-mozilla .cm-def,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-property,
+.cm-s-mozilla .cm-qualifier { /* blue */
   color: hsl(208,56%,40%);
 }
 
-.theme-fg-color3 { /* dark blue */
+.theme-fg-color3,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header { /* dark blue */
   color: hsl(208,81%,21%)
 }
 
-.theme-fg-color4 { /* Orange */
+.theme-fg-color4,
+.cm-s-mozilla .cm-comment { /* Orange */
   color: hsl(24,85%,39%);
 }
 
-.theme-fg-color5 { /* Yellow */
+.theme-fg-color5,
+.cm-s-mozilla .cm-bracket,
+.cm-s-mozilla .cm-keyword,
+.cm-s-mozilla .cm-atom { /* Yellow */
   color: #a18650;
 }
 
-.theme-fg-color6 { /* Orange */
+.theme-fg-color6,
+.cm-s-mozilla .cm-string { /* Orange */
   color: hsl(24,85%,39%);
 }
 
-.theme-fg-color7 { /* Red */
+.theme-fg-color7,
+.cm-s-mozilla .CodeMirror-nonmatchingbracket,
+.cm-s-mozilla .cm-string-2,
+.cm-s-mozilla .cm-error { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px #EFEFEF;
 }
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror { /* Inherit platform specific font sizing and styles */
+  font-family: inherit;
+  font-size: inherit;
+  background: transparent;
+}
+
+.CodeMirror pre,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special,
+.cm-s-mozilla .cm-number { /* theme-body color */
+  color: black;
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+  border-left: solid 1px black;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+  background: rgb(185, 215, 253);
+}
+
+.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+  background: rgb(176, 176, 176);
+}
+
+.CodeMirror-activeline-background { /* selected color with alpha */
+  background: rgba(185, 215, 253, .4);
+}
+
+.cm-s-markup-view pre {
+  line-height: 1.4em;
+  min-height: 1.4em;
+}