Bug 1095521 - Implement more paste commands for the inspector panel. r=pbrosset
authorfayolle-florent@orange.fr <fayolle-florent@orange.fr>
Sat, 22 Nov 2014 08:48:00 +0100
changeset 241416 d3032b990c98363c75e3e1c5eba0d82892bc3b24
parent 241415 c3d184a01153220e3ac00406ef09af0d451b6c03
child 241417 a7bd9b15a0715d881f8fa6d727be58839d09cace
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs1095521
milestone36.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 1095521 - Implement more paste commands for the inspector panel. r=pbrosset
browser/devtools/framework/selection.js
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/inspector.xul
browser/devtools/inspector/test/browser.ini
browser/devtools/inspector/test/browser_inspector_menu-01.js
browser/devtools/inspector/test/browser_inspector_menu-02.js
browser/devtools/inspector/test/browser_inspector_menu.js
browser/devtools/inspector/test/doc_inspector_menu-01.html
browser/devtools/inspector/test/doc_inspector_menu-02.html
browser/devtools/inspector/test/doc_inspector_menu.html
browser/devtools/inspector/test/head.js
browser/devtools/markupview/markup-view.js
browser/locales/en-US/chrome/browser/devtools/inspector.dtd
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/actors/root.js
--- a/browser/devtools/framework/selection.js
+++ b/browser/devtools/framework/selection.js
@@ -246,17 +246,17 @@ Selection.prototype = {
       }
       node = node.parentNode();
     };
     return false;
   },
 
   isHTMLNode: function() {
     let xhtml_ns = "http://www.w3.org/1999/xhtml";
-    return this.isNode() && this.node.namespaceURI == xhtml_ns;
+    return this.isNode() && this.nodeFront.namespaceURI == xhtml_ns;
   },
 
   // Node type
 
   isElementNode: function() {
     return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE;
   },
 
@@ -295,16 +295,34 @@ Selection.prototype = {
   isCommentNode: function() {
     return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
   },
 
   isDocumentNode: function() {
     return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE;
   },
 
+  /**
+   * @returns true if the selection is the <body> HTML element.
+   */
+  isBodyNode: function() {
+    return this.isHTMLNode() &&
+           this.isConnected() &&
+           this.nodeFront.nodeName === "BODY";
+  },
+
+  /**
+   * @returns true if the selection is the <head> HTML element.
+   */
+  isHeadNode: function() {
+    return this.isHTMLNode() &&
+           this.isConnected() &&
+           this.nodeFront.nodeName === "HEAD";
+  },
+
   isDocumentTypeNode: function() {
     return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE;
   },
 
   isDocumentFragmentNode: function() {
     return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE;
   },
 
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -52,17 +52,16 @@ const LAYOUT_CHANGE_TIMER = 250;
  *      Fired when the rule view updates to a new node
  */
 function InspectorPanel(iframeWindow, toolbox) {
   this._toolbox = toolbox;
   this._target = toolbox._target;
   this.panelDoc = iframeWindow.document;
   this.panelWin = iframeWindow;
   this.panelWin.inspector = this;
-  this._inspector = null;
 
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
   this._target.on("will-navigate", this._onBeforeNavigate);
 
   EventEmitter.decorate(this);
 }
 
 exports.InspectorPanel = InspectorPanel;
@@ -104,16 +103,20 @@ InspectorPanel.prototype = {
   get hasUrlToImageDataResolver() {
     return this._target.client.traits.urlToImageDataResolver;
   },
 
   get canGetUniqueSelector() {
     return this._target.client.traits.getUniqueSelector;
   },
 
+  get canPasteInnerOrAdjacentHTML() {
+    return this._target.client.traits.pasteHTML;
+  },
+
   _deferredOpen: function(defaultSelection) {
     let deferred = promise.defer();
 
     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;
@@ -568,17 +571,17 @@ InspectorPanel.prototype = {
   hideNodeMenu: function InspectorPanel_hideNodeMenu() {
     this.nodemenu.hidePopup();
   },
 
   /**
    * Returns the clipboard content if it is appropriate for pasting
    * into the current node's outer HTML, otherwise returns null.
    */
-  _getClipboardContentForOuterHTML: function Inspector_getClipboardContentForOuterHTML() {
+  _getClipboardContentForPaste: function Inspector_getClipboardContentForPaste() {
     let flavors = clipboard.currentFlavors;
     if (flavors.indexOf("text") != -1 ||
         (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
       let content = clipboard.get();
       if (content && content.trim().length > 0) {
         return content;
       }
     }
@@ -637,25 +640,44 @@ InspectorPanel.prototype = {
     // actor has the appropriate trait (isOuterHTMLEditable)
     let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
     if (isEditableElement && this.isOuterHTMLEditable) {
       editHTML.removeAttribute("disabled");
     } else {
       editHTML.setAttribute("disabled", "true");
     }
 
-    // Enable the "paste outer HTML" item if the selection is an element and
-    // the root actor has the appropriate trait (isOuterHTMLEditable) and if
-    // the clipbard content is appropriate.
     let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml");
-    if (isEditableElement && this.isOuterHTMLEditable &&
-        this._getClipboardContentForOuterHTML()) {
-      pasteOuterHTML.removeAttribute("disabled");
+    let pasteInnerHTML = this.panelDoc.getElementById("node-menu-pasteinnerhtml");
+    let pasteBefore = this.panelDoc.getElementById("node-menu-pastebefore");
+    let pasteAfter = this.panelDoc.getElementById("node-menu-pasteafter");
+    let pasteFirstChild = this.panelDoc.getElementById("node-menu-pastefirstchild");
+    let pasteLastChild = this.panelDoc.getElementById("node-menu-pastelastchild");
+
+    // Is the clipboard content appropriate? Is the element editable?
+    if (isEditableElement && this._getClipboardContentForPaste()) {
+      pasteInnerHTML.disabled = !this.canPasteInnerOrAdjacentHTML;
+      // Enable the "paste outer HTML" item if the selection is an element and
+      // the root actor has the appropriate trait (isOuterHTMLEditable).
+      pasteOuterHTML.disabled = !this.isOuterHTMLEditable;
+      // Don't paste before / after a root or a BODY or a HEAD element.
+      pasteBefore.disabled = pasteAfter.disabled =
+        !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() ||
+        this.selection.isBodyNode() || this.selection.isHeadNode();
+      // Don't paste as a first / last child of a HTML document element.
+      pasteFirstChild.disabled = pasteLastChild.disabled =
+        !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() &&
+        this.selection.isRoot());
     } else {
-      pasteOuterHTML.setAttribute("disabled", "true");
+      pasteOuterHTML.disabled = true;
+      pasteInnerHTML.disabled = true;
+      pasteBefore.disabled = true;
+      pasteAfter.disabled = true;
+      pasteFirstChild.disabled = true;
+      pasteLastChild.disabled = true;
     }
 
     // Enable the "copy image data-uri" item if the selection is previewable
     // which essentially checks if it's an image or canvas tag
     let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri");
     let markupContainer = this.markup.getContainer(this.selection.nodeFront);
     if (isSelectionElement && markupContainer && markupContainer.isPreviewable()) {
       copyImageData.removeAttribute("disabled");
@@ -685,17 +707,17 @@ InspectorPanel.prototype = {
 
     // This is needed to enable tooltips inside the iframe document.
     this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
     this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
 
     this._markupBox.setAttribute("collapsed", true);
     this._markupBox.appendChild(this._markupFrame);
     this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
-    this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView"))
+    this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView"));
   },
 
   _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
     this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
     delete this._boundMarkupFrameLoad;
 
     this._markupFrame.contentWindow.focus();
 
@@ -768,45 +790,71 @@ InspectorPanel.prototype = {
       return;
     }
     return this.walker.clearPseudoClassLocks().then(null, console.error);
   },
 
   /**
    * Edit the outerHTML of the selected Node.
    */
-  editHTML: function InspectorPanel_editHTML()
-  {
+  editHTML: function InspectorPanel_editHTML() {
     if (!this.selection.isNode()) {
       return;
     }
     if (this.markup) {
       this.markup.beginEditingOuterHTML(this.selection.nodeFront);
     }
   },
 
   /**
    * Paste the contents of the clipboard into the selected Node's outer HTML.
    */
-  pasteOuterHTML: function InspectorPanel_pasteOuterHTML()
-  {
-    let content = this._getClipboardContentForOuterHTML();
-    if (content) {
-      let node = this.selection.nodeFront;
-      this.markup.getNodeOuterHTML(node).then((oldContent) => {
-        this.markup.updateNodeOuterHTML(node, content, oldContent);
-      });
-    }
+  pasteOuterHTML: function InspectorPanel_pasteOuterHTML() {
+    let content = this._getClipboardContentForPaste();
+    if (!content)
+      return promise.reject("No clipboard content for paste");
+
+    let node = this.selection.nodeFront;
+    return this.markup.getNodeOuterHTML(node).then(oldContent => {
+      this.markup.updateNodeOuterHTML(node, content, oldContent);
+    });
+  },
+
+  /**
+   * Paste the contents of the clipboard into the selected Node's inner HTML.
+   */
+  pasteInnerHTML: function InspectorPanel_pasteInnerHTML() {
+    let content = this._getClipboardContentForPaste();
+    if (!content)
+      return promise.reject("No clipboard content for paste");
+
+    let node = this.selection.nodeFront;
+    return this.markup.getNodeInnerHTML(node).then(oldContent => {
+      this.markup.updateNodeInnerHTML(node, content, oldContent);
+    });
+  },
+
+  /**
+   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
+   * @param position The position as specified for Element.insertAdjacentHTML
+   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
+   */
+  pasteAdjacentHTML: function InspectorPanel_pasteAdjacent(position) {
+    let content = this._getClipboardContentForPaste();
+    if (!content)
+      return promise.reject("No clipboard content for paste");
+
+    let node = this.selection.nodeFront;
+    return this.markup.insertAdjacentHTMLToNode(node, position, content);
   },
 
   /**
    * Copy the innerHTML of the selected Node to the clipboard.
    */
-  copyInnerHTML: function InspectorPanel_copyInnerHTML()
-  {
+  copyInnerHTML: function InspectorPanel_copyInnerHTML() {
     if (!this.selection.isNode()) {
       return;
     }
     this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
   },
 
   /**
    * Copy the outerHTML of the selected Node to the clipboard.
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -54,20 +54,46 @@
         oncommand="inspector.copyUniqueSelector()"/>
       <menuitem id="node-menu-copyimagedatauri"
         label="&inspectorCopyImageDataUri.label;"
         oncommand="inspector.copyImageDataUri()"/>
       <menuitem id="node-menu-showdomproperties"
         label="&inspectorShowDOMProperties.label;"
         oncommand="inspector.showDOMProperties()"/>
       <menuseparator/>
+      <menuitem id="node-menu-pasteinnerhtml"
+        label="&inspectorHTMLPasteInner.label;"
+        accesskey="&inspectorHTMLPasteInner.accesskey;"
+        oncommand="inspector.pasteInnerHTML()"/>
       <menuitem id="node-menu-pasteouterhtml"
         label="&inspectorHTMLPasteOuter.label;"
         accesskey="&inspectorHTMLPasteOuter.accesskey;"
         oncommand="inspector.pasteOuterHTML()"/>
+      <menu id="node-menu-paste-extra-submenu"
+        label="&inspectorHTMLPasteExtraSubmenu.label;"
+        accesskey="&inspectorHTMLPasteExtraSubmenu.accesskey;">
+        <menupopup>
+          <menuitem id="node-menu-pastebefore"
+            label="&inspectorHTMLPasteBefore.label;"
+            accesskey="&inspectorHTMLPasteBefore.accesskey;"
+            oncommand="inspector.pasteAdjacentHTML('beforeBegin')"/>
+          <menuitem id="node-menu-pasteafter"
+            label="&inspectorHTMLPasteAfter.label;"
+            accesskey="&inspectorHTMLPasteAfter.accesskey;"
+            oncommand="inspector.pasteAdjacentHTML('afterEnd')"/>
+          <menuitem id="node-menu-pastefirstchild"
+            label="&inspectorHTMLPasteFirstChild.label;"
+            accesskey="&inspectorHTMLPasteFirstChild.accesskey;"
+            oncommand="inspector.pasteAdjacentHTML('afterBegin')"/>
+          <menuitem id="node-menu-pastelastchild"
+            label="&inspectorHTMLPasteLastChild.label;"
+            accesskey="&inspectorHTMLPasteLastChild.accesskey;"
+            oncommand="inspector.pasteAdjacentHTML('beforeEnd')"/>
+        </menupopup>
+      </menu>
       <menuseparator/>
       <menuitem id="node-menu-delete"
         label="&inspectorHTMLDelete.label;"
         accesskey="&inspectorHTMLDelete.accesskey;"
         oncommand="inspector.deleteNode()"/>
       <menuseparator/>
       <menuitem id="node-menu-pseudo-hover"
         label=":hover" type="checkbox"
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -10,17 +10,18 @@ support-files =
   doc_inspector_highlight_after_transition.html
   doc_inspector_highlighter-comments.html
   doc_inspector_highlighter_csstransform.html
   doc_inspector_highlighter.html
   doc_inspector_highlighter_rect.html
   doc_inspector_highlighter_rect_iframe.html
   doc_inspector_infobar_01.html
   doc_inspector_infobar_02.html
-  doc_inspector_menu.html
+  doc_inspector_menu-01.html
+  doc_inspector_menu-02.html
   doc_inspector_remove-iframe-during-load.html
   doc_inspector_search.html
   doc_inspector_search-suggestions.html
   doc_inspector_select-last-selected-01.html
   doc_inspector_select-last-selected-02.html
   head.js
 
 [browser_inspector_breadcrumbs.js]
@@ -49,17 +50,18 @@ support-files =
 [browser_inspector_highlighter-selector_02.js]
 [browser_inspector_highlighter-zoom.js]
 [browser_inspector_iframe-navigation.js]
 [browser_inspector_infobar_01.js]
 [browser_inspector_initialization.js]
 [browser_inspector_inspect-object-element.js]
 [browser_inspector_invalidate.js]
 [browser_inspector_keyboard-shortcuts.js]
-[browser_inspector_menu.js]
+[browser_inspector_menu-01.js]
+[browser_inspector_menu-02.js]
 [browser_inspector_navigation.js]
 [browser_inspector_picker-stop-on-destroy.js]
 [browser_inspector_picker-stop-on-tool-change.js]
 [browser_inspector_pseudoclass-lock.js]
 [browser_inspector_pseudoclass-menu.js]
 [browser_inspector_reload-01.js]
 [browser_inspector_reload-02.js]
 [browser_inspector_remove-iframe-during-load.js]
rename from browser/devtools/inspector/test/browser_inspector_menu.js
rename to browser/devtools/inspector/test/browser_inspector_menu-01.js
--- a/browser/devtools/inspector/test/browser_inspector_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_menu-01.js
@@ -9,65 +9,30 @@ http://creativecommons.org/publicdomain/
 // As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is not a function");
 
 // Test context menu functionality:
 // 1) menu items are disabled/enabled depending on the clicked node
 // 2) actions triggered by the items work correctly
 
-const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu.html";
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-01.html";
 const MENU_SENSITIVITY_TEST_DATA = [
   {
     desc: "doctype node",
     selector: null,
     disabled: true,
   },
   {
     desc: "element node",
     selector: "p",
     disabled: false,
   }
 ];
 
-const PASTE_OUTER_HTML_TEST_DATA = [
-  {
-    desc: "some text",
-    clipboardData: "some text",
-    clipboardDataType: undefined,
-    disabled: false
-  },
-  {
-    desc: "base64 encoded image data uri",
-    clipboardData:
-      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
-      "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
-    clipboardDataType: undefined,
-    disabled: true
-  },
-  {
-    desc: "html",
-    clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
-    disabled: false
-  },
-  {
-    desc: "empty string",
-    clipboardData: "",
-    clipboardDataType: undefined,
-    disabled: true
-  },
-  {
-    desc: "whitespace only",
-    clipboardData: " \n\n\t\n\n  \n",
-    clipboardDataType: undefined,
-    disabled: true
-  },
-];
-
 const COPY_ITEMS_TEST_DATA = [
   {
     desc: "copy inner html",
     id: "node-menu-copyinner",
     text: "This is some example text",
   },
   {
     desc: "copy outer html",
@@ -85,40 +50,35 @@ let clipboard = require("sdk/clipboard")
 registerCleanupFunction(() => {
   clipboard = null;
 });
 
 add_task(function* () {
   let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
 
   yield testMenuItemSensitivity();
-  yield testPasteOuterHTMLMenuItemSensitivity();
   yield testCopyMenuItems();
   yield testShowDOMProperties();
-  yield testPasteOuterHTMLMenu();
   yield testDeleteNode();
   yield testDeleteRootNode();
 
   function* testMenuItemSensitivity() {
     info("Testing sensitivity of menu items for different elements.");
 
+    // The sensibility for paste options are described in browser_inspector_menu-02.js
     const MENU_ITEMS = [
       "node-menu-copyinner",
       "node-menu-copyouter",
       "node-menu-copyuniqueselector",
       "node-menu-delete",
-      "node-menu-pasteouterhtml",
       "node-menu-pseudo-hover",
       "node-menu-pseudo-active",
       "node-menu-pseudo-focus"
     ];
 
-    // To ensure clipboard contains something to paste.
-    clipboard.set("<p>test</p>", "html");
-
     for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) {
       info("Testing context menu entries for " + desc);
 
       let front;
       if (selector) {
         front = yield getNodeFront(selector, inspector);
       } else {
         // Select the docType if no selector is provided
@@ -130,35 +90,16 @@ add_task(function* () {
       contextMenuClick(getContainerForNodeFront(front, inspector).tagLine);
 
       for (let name of MENU_ITEMS) {
         checkMenuItem(name, disabled);
       }
     }
   }
 
-  function* testPasteOuterHTMLMenuItemSensitivity() {
-    info("Checking 'Paste Outer HTML' menu item sensitivity for different types" +
-         "of data");
-
-    let nodeFront = yield getNodeFront("p", inspector);
-    let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
-
-    for (let data of PASTE_OUTER_HTML_TEST_DATA) {
-      let { desc, clipboardData, clipboardDataType, disabled } = data;
-      info("Checking 'Paste Outer HTML' for " + desc);
-      clipboard.set(clipboardData, clipboardDataType);
-
-      yield selectNode(nodeFront, inspector);
-
-      contextMenuClick(markupTagLine);
-      checkMenuItem("node-menu-pasteouterhtml", disabled);
-    }
-  }
-
   function* testCopyMenuItems() {
     info("Testing various copy actions of context menu.");
     for (let {desc, id, text} of COPY_ITEMS_TEST_DATA) {
       info("Testing " + desc);
 
       let item = inspector.panelDoc.getElementById(id);
       ok(item, "The popup has a " + desc + " menu item.");
 
@@ -185,37 +126,16 @@ add_task(function* () {
     yield messagesAdded;
 
     info("Checking if 'inspect($0)' was evaluated");
     ok(webconsoleUI.jsterm.history[0] === 'inspect($0)');
 
     yield toolbox.toggleSplitConsole();
   }
 
-  function* testPasteOuterHTMLMenu() {
-    info("Testing that 'Paste Outer HTML' menu item works.");
-    clipboard.set("this was pasted");
-
-    let nodeFront = yield getNodeFront("h1", inspector);
-    yield selectNode(nodeFront, inspector);
-
-    contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
-
-    let onNodeReselected = inspector.markup.once("reselectedonremoved");
-    let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
-    dispatchCommandEvent(menu);
-
-    info("Waiting for inspector selection to update");
-    yield onNodeReselected;
-
-    ok(content.document.body.outerHTML.contains(clipboard.get()),
-       "Clipboard content was pasted into the node's outer HTML.");
-    ok(!getNode("h1", { expectNoMatch: true }), "The original node was removed.");
-  }
-
   function* testDeleteNode() {
     info("Testing 'Delete Node' menu item for normal elements.");
 
     yield selectNode("p", inspector);
     let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
     ok(deleteNode, "the popup menu has a delete menu item");
 
     let updated = inspector.once("inspector-updated");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_menu-02.js
@@ -0,0 +1,326 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test context menu functionality:
+// 1) menu items are disabled/enabled depending on the clicked node
+// 2) actions triggered by the items work correctly
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is not a function");
+
+const MENU_SENSITIVITY_TEST_DATA = [
+  {
+    desc: "doctype node",
+    selector: null,
+    disabled: true,
+  },
+  {
+    desc: "element node",
+    selector: "#sensitivity",
+    disabled: false,
+  },
+  {
+    desc: "document element",
+    selector: "html",
+    disabled: {
+      "node-menu-pastebefore": true,
+      "node-menu-pasteafter": true,
+      "node-menu-pastefirstchild": true,
+      "node-menu-pastelastchild": true,
+    }
+  },
+  {
+    desc: "body",
+    selector: "body",
+    disabled: {
+      "node-menu-pastebefore": true,
+      "node-menu-pasteafter": true,
+    }
+  },
+  {
+    desc: "head",
+    selector: "head",
+    disabled: {
+      "node-menu-pastebefore": true,
+      "node-menu-pasteafter": true,
+    }
+  }
+];
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-02.html";
+
+const PASTE_HTML_TEST_SENSITIVITY_DATA = [
+  {
+    desc: "some text",
+    clipboardData: "some text",
+    clipboardDataType: undefined,
+    disabled: false
+  },
+  {
+    desc: "base64 encoded image data uri",
+    clipboardData:
+      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
+      "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
+    clipboardDataType: undefined,
+    disabled: true
+  },
+  {
+    desc: "html",
+    clipboardData: "<p>some text</p>",
+    clipboardDataType: "html",
+    disabled: false
+  },
+  {
+    desc: "empty string",
+    clipboardData: "",
+    clipboardDataType: undefined,
+    disabled: true
+  },
+  {
+    desc: "whitespace only",
+    clipboardData: " \n\n\t\n\n  \n",
+    clipboardDataType: undefined,
+    disabled: true
+  },
+];
+
+const PASTE_ADJACENT_HTML_DATA = [
+  {
+    desc: "As First Child",
+    clipboardData: "2",
+    menuId: "node-menu-pastefirstchild",
+  },
+  {
+    desc: "As Last Child",
+    clipboardData: "4",
+    menuId: "node-menu-pastelastchild",
+  },
+  {
+    desc: "Before",
+    clipboardData: "1",
+    menuId: "node-menu-pastebefore",
+  },
+  {
+    desc: "After",
+    clipboardData: "<span>5</span>",
+    menuId: "node-menu-pasteafter",
+  },
+];
+
+
+let clipboard = require("sdk/clipboard");
+registerCleanupFunction(() => {
+  clipboard = null;
+});
+
+add_task(function* () {
+  let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
+
+  yield testMenuItemSensitivity();
+  yield testPasteHTMLMenuItemsSensitivity();
+  yield testPasteOuterHTMLMenu();
+  yield testPasteInnerHTMLMenu();
+  yield testPasteAdjacentHTMLMenu();
+
+  function* testMenuItemSensitivity() {
+    info("Testing sensitivity of menu items for different elements.");
+
+    const MENU_ITEMS = [
+      "node-menu-pasteinnerhtml",
+      "node-menu-pasteouterhtml",
+      "node-menu-pastebefore",
+      "node-menu-pasteafter",
+      "node-menu-pastefirstchild",
+      "node-menu-pastelastchild",
+    ];
+
+    // To ensure clipboard contains something to paste.
+    clipboard.set("<p>test</p>", "html");
+
+    for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) {
+      info("Testing context menu entries for " + desc);
+
+      let front;
+      if (selector) {
+        front = yield getNodeFront(selector, inspector);
+      } else {
+        // Select the docType if no selector is provided
+        let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
+        front = nodes[0];
+      }
+      yield selectNode(front, inspector);
+
+      contextMenuClick(getContainerForNodeFront(front, inspector).tagLine);
+
+      for (let name of MENU_ITEMS) {
+        let disabledForMenu = typeof disabled === "object" ?
+          disabled[name] : disabled;
+        info(`${name} should be ${disabledForMenu ? "disabled" : "enabled"}  ` +
+          `for ${desc}`);
+        checkMenuItem(name, disabledForMenu);
+      }
+    }
+  }
+
+  function* testPasteHTMLMenuItemsSensitivity() {
+    let menus = [
+      "node-menu-pasteinnerhtml",
+      "node-menu-pasteouterhtml",
+      "node-menu-pastebefore",
+      "node-menu-pasteafter",
+      "node-menu-pastefirstchild",
+      "node-menu-pastelastchild",
+    ];
+
+    info("Checking Paste menu items sensitivity for different types" +
+         "of data");
+
+    let nodeFront = yield getNodeFront("#paste-area", inspector);
+    let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
+
+    for (let menuId of menus) {
+      for (let data of PASTE_HTML_TEST_SENSITIVITY_DATA) {
+        let { desc, clipboardData, clipboardDataType, disabled } = data;
+        let menuLabel = getLabelFor("#" + menuId);
+        info(`Checking ${menuLabel} for ${desc}`);
+        clipboard.set(clipboardData, clipboardDataType);
+
+        yield selectNode(nodeFront, inspector);
+
+        contextMenuClick(markupTagLine);
+        checkMenuItem(menuId, disabled);
+      }
+    }
+  }
+
+  function* testPasteOuterHTMLMenu() {
+    info("Testing that 'Paste Outer HTML' menu item works.");
+    clipboard.set("this was pasted (outerHTML)");
+    let outerHTMLSelector = "#paste-area h1";
+
+    let nodeFront = yield getNodeFront(outerHTMLSelector, inspector);
+    yield selectNode(nodeFront, inspector);
+
+    contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
+
+    let onNodeReselected = inspector.markup.once("reselectedonremoved");
+    let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
+    dispatchCommandEvent(menu);
+
+    info("Waiting for inspector selection to update");
+    yield onNodeReselected;
+
+    ok(content.document.body.outerHTML.contains(clipboard.get()),
+       "Clipboard content was pasted into the node's outer HTML.");
+    ok(!getNode(outerHTMLSelector, { expectNoMatch: true }),
+      "The original node was removed.");
+  }
+
+  function* testPasteInnerHTMLMenu() {
+    info("Testing that 'Paste Inner HTML' menu item works.");
+    clipboard.set("this was pasted (innerHTML)");
+    let innerHTMLSelector = "#paste-area .inner";
+    let getInnerHTML = () => content.document.querySelector(innerHTMLSelector).innerHTML;
+    let origInnerHTML = getInnerHTML();
+
+    let nodeFront = yield getNodeFront(innerHTMLSelector, inspector);
+    yield selectNode(nodeFront, inspector);
+
+    contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
+
+    let onMutation = inspector.once("markupmutation");
+    let menu = inspector.panelDoc.getElementById("node-menu-pasteinnerhtml");
+    dispatchCommandEvent(menu);
+
+    info("Waiting for mutation to occur");
+    yield onMutation;
+
+    ok(getInnerHTML() === clipboard.get(),
+       "Clipboard content was pasted into the node's inner HTML.");
+    ok(getNode(innerHTMLSelector), "The original node has been preserved.");
+    yield undoChange(inspector);
+    ok(getInnerHTML() === origInnerHTML, "Previous innerHTML has been " +
+      "restored after undo");
+  }
+
+  function* testPasteAdjacentHTMLMenu() {
+    let refSelector = "#paste-area .adjacent .ref";
+    let adjacentNode = content.document.querySelector(refSelector).parentNode;
+    let nodeFront = yield getNodeFront(refSelector, inspector);
+    yield selectNode(nodeFront, inspector);
+    let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
+
+    for (let { desc, clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) {
+      let menu = inspector.panelDoc.getElementById(menuId);
+      info(`Testing ${getLabelFor(menu)} for ${clipboardData}`);
+      clipboard.set(clipboardData);
+
+      contextMenuClick(markupTagLine);
+      let onMutation = inspector.once("markupmutation");
+      dispatchCommandEvent(menu);
+
+      info("Waiting for mutation to occur");
+      yield onMutation;
+    }
+
+    ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>" +
+      "<span>5</span>", "The Paste as Last Child / as First Child / Before " +
+      "/ After worked as expected");
+    yield undoChange(inspector);
+    ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>",
+      "Undo works for paste adjacent HTML");
+  }
+
+  function checkMenuItem(elementId, disabled) {
+    if (disabled) {
+      checkDisabled(elementId);
+    } else {
+      checkEnabled(elementId);
+    }
+  }
+
+  function checkEnabled(elementId) {
+    let elt = inspector.panelDoc.getElementById(elementId);
+    ok(!elt.hasAttribute("disabled"),
+      '"' + elt.label + '" context menu option is not disabled');
+  }
+
+  function checkDisabled(elementId) {
+    let elt = inspector.panelDoc.getElementById(elementId);
+    ok(elt.hasAttribute("disabled"),
+      '"' + elt.label + '" context menu option is disabled');
+  }
+
+  function dispatchCommandEvent(node) {
+    info("Dispatching command event on " + node);
+    let commandEvent = document.createEvent("XULCommandEvent");
+    commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
+                                  false, false, null);
+    node.dispatchEvent(commandEvent);
+  }
+
+  function contextMenuClick(element) {
+    info("Simulating contextmenu event on " + element);
+    let evt = element.ownerDocument.createEvent('MouseEvents');
+    let button = 2;  // right click
+
+    evt.initMouseEvent('contextmenu', true, true,
+         element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false,
+         false, false, false, button, null);
+
+    element.dispatchEvent(evt);
+  }
+
+  function getLabelFor(elt) {
+    if (typeof elt === "string")
+      elt = inspector.panelDoc.querySelector(elt);
+    let isInPasteSubMenu = elt.matches("#node-menu-paste-extra-submenu *");
+    return `"${isInPasteSubMenu ? "Paste > " : ""}${elt.label}"`;
+  }
+});
rename from browser/devtools/inspector/test/doc_inspector_menu.html
rename to browser/devtools/inspector/test/doc_inspector_menu-01.html
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_menu-02.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Inspector Tree Menu Test</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <div>
+      <div id="paste-area">
+        <h1>Inspector Tree Menu Test</h1>
+        <p class="inner">Unset</p>
+        <p class="adjacent">
+          <span class="ref">3</span>
+        </p>
+      </div>
+      <p data-id="copy">Paragraph for testing copy</p>
+      <p id="sensitivity">Paragraph for sensitivity</p>
+      <p id="delete">This has to be deleted</p>
+    </div>
+  </body>
+</html>
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -659,8 +659,48 @@ function executeInContent(name, data={},
 
   mm.sendAsyncMessage(name, data, objects);
   if (expectResponse) {
     return waitForContentMessage(name);
   } else {
     return promise.resolve();
   }
 }
+
+/**
+ * Undo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no undo action is possible
+ */
+function undoChange(inspector) {
+  let canUndo = inspector.markup.undo.canUndo();
+  ok(canUndo, "The last change in the markup-view can be undone");
+  if (!canUndo) {
+    return promise.reject();
+  }
+
+  let mutated = inspector.once("markupmutation");
+  inspector.markup.undo.undo();
+  return mutated;
+}
+
+/**
+ * Redo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no redo action is possible
+ */
+function redoChange(inspector) {
+  let canRedo = inspector.markup.undo.canRedo();
+  ok(canRedo, "The last change in the markup-view can be redone");
+  if (!canRedo) {
+    return promise.reject();
+  }
+
+  let mutated = inspector.once("markupmutation");
+  inspector.markup.undo.redo();
+  return mutated;
+}
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -28,17 +28,17 @@ Cu.import("resource://gre/modules/devtoo
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
 loader.lazyGetter(this, "AutocompletePopup", () => {
-  return require("devtools/shared/autocomplete-popup").AutocompletePopup
+  return require("devtools/shared/autocomplete-popup").AutocompletePopup;
 });
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  *  - MarkupElementContainer: markup container for element nodes
@@ -697,26 +697,29 @@ MarkupView.prototype = {
         if (type === "attributes" || type === "characterData") {
           addedOrEditedContainers.add(container);
         } else if (type === "childList") {
           // If there has been removals, flash the parent
           if (removed.length) {
             removedContainers.add(container);
           }
 
-          // If there has been additions, flash the nodes
+          // If there has been additions, flash the nodes if their associated
+          // container exist (so if their parent is expanded in the inspector).
           added.forEach(added => {
             let addedContainer = this.getContainer(added);
-            addedOrEditedContainers.add(addedContainer);
+            if (addedContainer) {
+              addedOrEditedContainers.add(addedContainer);
 
-            // The node may be added as a result of an append, in which case it
-            // it will have been removed from another container first, but in
-            // these cases we don't want to flash both the removal and the
-            // addition
-            removedContainers.delete(container);
+              // The node may be added as a result of an append, in which case
+              // it will have been removed from another container first, but in
+              // these cases we don't want to flash both the removal and the
+              // addition
+              removedContainers.delete(container);
+            }
           });
         }
       }
     }
 
     for (let container of removedContainers) {
       container.flashMutation();
     }
@@ -796,29 +799,55 @@ MarkupView.prototype = {
    * Collapse the node's children.
    */
   collapseNode: function(aNode) {
     let container = this.getContainer(aNode);
     container.expanded = false;
   },
 
   /**
+   * Returns either the innerHTML or the outerHTML for a remote node.
+   * @param aNode The NodeFront to get the outerHTML / innerHTML for.
+   * @param isOuter A boolean that, if true, makes the function return the
+   *                outerHTML, otherwise the innerHTML.
+   * @returns A promise that will be resolved with the outerHTML / innerHTML.
+   */
+  _getNodeHTML: function(aNode, isOuter) {
+    let walkerPromise = null;
+
+    if (isOuter) {
+      walkerPromise = this.walker.outerHTML(aNode);
+    } else {
+      walkerPromise = this.walker.innerHTML(aNode);
+    }
+
+    return walkerPromise.then(longstr => {
+      return longstr.string().then(html => {
+        longstr.release().then(null, console.error);
+        return html;
+      });
+    });
+  },
+
+  /**
    * 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;
+    return this._getNodeHTML(aNode, true);
+  },
+
+  /**
+   * Retrieve the innerHTML for a remote node.
+   * @param aNode The NodeFront to get the innerHTML for.
+   * @returns A promise that will be resolved with the innerHTML.
+   */
+  getNodeInnerHTML: function(aNode) {
+    return this._getNodeHTML(aNode);
   },
 
   /**
    * Listen to mutations, expect a given node to be removed and try and select
    * the node that sits at the same place instead.
    * This is useful when changing the outerHTML or the tag name so that the
    * newly inserted node gets selected instead of the one that just got removed.
    */
@@ -881,41 +910,99 @@ MarkupView.prototype = {
       this._removedNodeObserver = null;
       this.emit("canceledreselectonremoved");
     }
   },
 
   /**
    * Replace the outerHTML of any node displayed in the inspector with
    * some other HTML code
-   * @param aNode node which outerHTML will be replaced.
-   * @param newValue The new outerHTML to set on the node.
-   * @param oldValue The old outerHTML that will be used if the user undos the update.
+   * @param {NodeFront} node node which outerHTML will be replaced.
+   * @param {string} newValue The new outerHTML to set on the node.
+   * @param {string} oldValue The old outerHTML that will be used if the
+   *                          user undoes the update.
    * @returns A promise that will resolve when the outer HTML has been updated.
    */
-  updateNodeOuterHTML: function(aNode, newValue, oldValue) {
-    let container = this._containers.get(aNode);
+  updateNodeOuterHTML: function(node, newValue, oldValue) {
+    let container = this.getContainer(node);
     if (!container) {
       return promise.reject();
     }
 
     // Changing the outerHTML removes the node which outerHTML was changed.
     // Listen to this removal to reselect the right node afterwards.
-    this.reselectOnRemoved(aNode, "outerhtml");
-    return this.walker.setOuterHTML(aNode, newValue).then(null, () => {
+    this.reselectOnRemoved(node, "outerhtml");
+    return this.walker.setOuterHTML(node, newValue).then(null, () => {
       this.cancelReselectOnRemoved();
     });
   },
 
   /**
+   * Replace the innerHTML of any node displayed in the inspector with
+   * some other HTML code
+   * @param {Node} node node which innerHTML will be replaced.
+   * @param {string} newValue The new innerHTML to set on the node.
+   * @param {string} oldValue The old innerHTML that will be used if the user
+   *                 undoes the update.
+   * @returns A promise that will resolve when the inner HTML has been updated.
+   */
+  updateNodeInnerHTML: function(node, newValue, oldValue) {
+    let container = this.getContainer(node);
+    if (!container) {
+      return promise.reject();
+    }
+
+    let def = promise.defer();
+
+    container.undo.do(() => {
+      this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject);
+    }, () => {
+      this.walker.setInnerHTML(node, oldValue);
+    });
+
+    return def.promise;
+  },
+
+  /**
+   * Insert adjacent HTML to any node displayed in the inspector.
+   *
+   * @param {NodeFront} node The reference node.
+   * @param {string} position The position as specified for Element.insertAdjacentHTML
+   *        (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
+   * @param {string} newValue The adjacent HTML.
+   * @returns A promise that will resolve when the adjacent HTML has
+   *          been inserted.
+   */
+  insertAdjacentHTMLToNode: function(node, position, value) {
+    let container = this.getContainer(node);
+    if (!container) {
+      return promise.reject();
+    }
+
+    let def = promise.defer();
+
+    let injectedNodes = [];
+    container.undo.do(() => {
+      this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => {
+        injectedNodes = nodeArray.nodes;
+        return nodeArray;
+      }).then(def.resolve, def.reject);
+    }, () => {
+      this.walker.removeNodes(injectedNodes);
+    });
+
+    return def.promise;
+  },
+
+  /**
    * 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)=> {
+    this.getNodeOuterHTML(aNode).then(oldValue => {
       let container = this.getContainer(aNode);
       if (!container) {
         return;
       }
       this.htmlEditor.show(container.tagLine, oldValue);
       this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
         // Need to focus the <html> element instead of the frame / window
         // in order to give keyboard focus back to doc (from editor).
@@ -1212,17 +1299,17 @@ MarkupView.prototype = {
 
     this._frame.contentWindow.removeEventListener("keydown",
       this._boundKeyDown, false);
     this._boundKeyDown = null;
 
     this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
     this._boundOnNewSelection = null;
 
-    this.walker.off("mutations", this._boundMutationObserver)
+    this.walker.off("mutations", this._boundMutationObserver);
     this._boundMutationObserver = null;
 
     this.walker.off("display-change", this._boundOnDisplayChange);
     this._boundOnDisplayChange = null;
 
     this._elt.removeEventListener("mousemove", this._onMouseMove, false);
     this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
     this._elt = null;
@@ -1919,17 +2006,17 @@ function TextEditor(aContainer, aNode, a
 
           this.container.undo.do(() => {
             this.node.setNodeValue(aVal).then(() => {
               this.markup.nodeChanged(this.node);
             });
           }, () => {
             this.node.setNodeValue(oldValue).then(() => {
               this.markup.nodeChanged(this.node);
-            })
+            });
           });
         });
       });
     }
   });
 
   this.update();
 }
@@ -2150,17 +2237,17 @@ ElementEditor.prototype = {
         try {
           this._saveAttribute(aAttr.name, undoMods);
           doMods.removeAttribute(aAttr.name);
           this._applyAttributes(aVal, attr, doMods, undoMods);
           this.container.undo.do(() => {
             doMods.apply();
           }, () => {
             undoMods.apply();
-          })
+          });
         } catch(ex) {
           console.error(ex);
         }
       }
     });
 
     // Figure out where we should place the attribute.
     let before = aBefore;
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
@@ -1,26 +1,96 @@
+<!-- LOCALIZATION NOTE (inspectorHTMLEdit.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users edit the
+     (outer) HTML of the current node -->
 <!ENTITY inspectorHTMLEdit.label       "Edit As HTML">
 <!ENTITY inspectorHTMLEdit.accesskey   "E">
 
+<!-- LOCALIZATION NOTE (inspectorHTMLCopyInner.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users copy the
+     inner HTML of the current node -->
 <!ENTITY inspectorHTMLCopyInner.label       "Copy Inner HTML">
 <!ENTITY inspectorHTMLCopyInner.accesskey   "I">
 
+<!-- LOCALIZATION NOTE (inspectorHTMLCopyOuter.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users copy the
+     outer HTML of the current node -->
 <!ENTITY inspectorHTMLCopyOuter.label       "Copy Outer HTML">
 <!ENTITY inspectorHTMLCopyOuter.accesskey   "O">
 
+<!-- LOCALIZATION NOTE (inspectorCopyUniqueSelector.label): This is the label
+     shown in the inspector contextual-menu for the item that lets users copy
+     the CSS Selector of the current node -->
 <!ENTITY inspectorCopyUniqueSelector.label       "Copy Unique Selector">
 <!ENTITY inspectorCopyUniqueSelector.accesskey   "U">
 
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteOuter.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users paste outer
+     HTML in the current node -->
 <!ENTITY inspectorHTMLPasteOuter.label      "Paste Outer HTML">
 <!ENTITY inspectorHTMLPasteOuter.accesskey  "P">
 
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteInner.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users paste inner
+     HTML in the current node -->
+<!ENTITY inspectorHTMLPasteInner.label      "Paste Inner HTML">
+<!ENTITY inspectorHTMLPasteInner.accesskey  "N">
+
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteExtraSubmenu.label): This is the label
+     shown in the inspector contextual-menu for the sub-menu of the other Paste
+     items, which allow to paste HTML:
+     - before the current node
+     - after the current node
+     - as the first child of the current node
+     - as the last child of the current node -->
+<!ENTITY inspectorHTMLPasteExtraSubmenu.label      "Paste ...">
+<!ENTITY inspectorHTMLPasteExtraSubmenu.accesskey  "T">
+
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteBefore.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users paste
+     the HTML before the current node -->
+<!ENTITY inspectorHTMLPasteBefore.label      "Before">
+<!ENTITY inspectorHTMLPasteBefore.accesskey  "B">
+
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteAfter.label): This is the label shown
+     in the inspector contextual-menu for the item that lets users paste
+     the HTML after the current node -->
+<!ENTITY inspectorHTMLPasteAfter.label       "After">
+<!ENTITY inspectorHTMLPasteAfter.accesskey   "A">
+
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteFirstChild.label): This is the label
+     shown in the inspector contextual-menu for the item that lets users paste
+     the HTML as the first child the current node -->
+<!ENTITY inspectorHTMLPasteFirstChild.label      "As First Child">
+<!ENTITY inspectorHTMLPasteFirstChild.accesskey  "F">
+
+<!-- LOCALIZATION NOTE (inspectorHTMLPasteLastChild.label): This is the label
+     shown in the inspector contextual-menu for the item that lets users paste
+     the HTML as the last child the current node -->
+<!ENTITY inspectorHTMLPasteLastChild.label       "As Last Child">
+<!ENTITY inspectorHTMLPasteLastChild.accesskey   "L">
+
+
+<!-- LOCALIZATION NOTE (inspectorHTMLDelete.label): This is the label shown in
+     the inspector contextual-menu for the item that lets users delete the
+     current node -->
 <!ENTITY inspectorHTMLDelete.label          "Delete Node">
 <!ENTITY inspectorHTMLDelete.accesskey      "D">
 
 <!ENTITY inspector.selectButton.tooltip     "Select element with mouse">
 
+<!-- LOCALIZATION NOTE (inspectorSearchHTML.label): This is the label shown as
+     the placeholder in inspector search box -->
 <!ENTITY inspectorSearchHTML.label          "Search HTML">
 <!ENTITY inspectorSearchHTML.key            "F">
 
+<!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label
+     shown in the inspector contextual-menu for the item that lets users copy
+     the URL embedding the image data encoded in Base 64 (what we name
+     here Image Data URL). For more information:
+     https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs -->
 <!ENTITY inspectorCopyImageDataUri.label       "Copy Image Data-URL">
 
+<!-- LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
+     shown in the inspector contextual-menu for the item that lets users see
+     the DOM properties of the current node. When triggered, this item
+     opens the split Console and displays the properties in its side panel. -->
 <!ENTITY inspectorShowDOMProperties.label       "Show DOM Properties">
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -1037,24 +1037,17 @@ var NodeListActor = exports.NodeListActo
     response: RetVal("disconnectedNode")
   }),
 
   /**
    * Get a range of the items from the node list.
    */
   items: method(function(start=0, end=this.nodeList.length) {
     let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))];
-    let newParents = new Set();
-    for (let item of items) {
-      this.walker.ensurePathToRoot(item, newParents);
-    }
-    return {
-      nodes: items,
-      newParents: [node for (node of newParents)]
-    }
+    return this.walker.attachElements(items);
   }, {
     request: {
       start: Arg(0, "nullable:number"),
       end: Arg(1, "nullable:number")
     },
     response: RetVal("disconnectedNodeArray")
   }),
 
@@ -1294,22 +1287,53 @@ var WalkerActor = protocol.ActorClass({
    *
    * Keeping these actor methods for now allows newer client-side debuggers to
    * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
    */
   pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}),
   cancelPick: method(function() {}),
   highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}),
 
+  /**
+   * Ensures that the node is attached and it can be accessed from the root.
+   *
+   * @param {(Node|NodeActor)} nodes The nodes
+   * @return {Object} An object compatible with the disconnectedNode type.
+   */
   attachElement: function(node) {
-    node = this._ref(node);
-    let newParents = this.ensurePathToRoot(node);
+    let { nodes, newParents } = this.attachElements([node]);
     return {
-      node: node,
-      newParents: [parent for (parent of newParents)]
+      node: nodes[0],
+      newParents: newParents
+    };
+  },
+
+  /**
+   * Ensures that the nodes are attached and they can be accessed from the root.
+   *
+   * @param {(Node[]|NodeActor[])} nodes The nodes
+   * @return {Object} An object compatible with the disconnectedNodeArray type.
+   */
+  attachElements: function(nodes) {
+    let nodeActors = [];
+    let newParents = new Set();
+    for (let node of nodes) {
+      // Be sure we deal with NodeActor only.
+      if (!(node instanceof NodeActor))
+        node = this._ref(node);
+
+      this.ensurePathToRoot(node, newParents);
+      // If nodes may be an array of raw nodes, we're sure to only have
+      // NodeActors with the following array.
+      nodeActors.push(node);
+    }
+
+    return {
+      nodes: nodeActors,
+      newParents: [...newParents]
     };
   },
 
   /**
    * Watch the given document node for mutations using the DOM observer
    * API.
    */
   _watchDocument: function(actor) {
@@ -2049,31 +2073,55 @@ var WalkerActor = protocol.ActorClass({
       node: Arg(0, "domnode")
     },
     response: {
       value: RetVal("longstring")
     }
   }),
 
   /**
+   * Set a node's innerHTML property.
+   *
+   * @param {NodeActor} node The node.
+   * @param {string} value The piece of HTML content.
+   */
+  setInnerHTML: method(function(node, value) {
+    let rawNode = node.rawNode;
+    if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE)
+      throw new Error("Can only change innerHTML to element nodes");
+    rawNode.innerHTML = value;
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      value: Arg(1, "string"),
+    },
+    response: {}
+  }),
+
+  /**
    * Get a node's outerHTML property.
+   *
+   * @param {NodeActor} node The node.
    */
   outerHTML: method(function(node) {
     return LongStringActor(this.conn, node.rawNode.outerHTML);
   }, {
     request: {
       node: Arg(0, "domnode")
     },
     response: {
       value: RetVal("longstring")
     }
   }),
 
   /**
    * Set a node's outerHTML property.
+   *
+   * @param {NodeActor} node The node.
+   * @param {string} value The piece of HTML content.
    */
   setOuterHTML: method(function(node, value) {
     let parsedDOM = DOMParser.parseFromString(value, "text/html");
     let rawNode = node.rawNode;
     let parentNode = rawNode.parentNode;
 
     // Special case for head and body.  Setting document.body.outerHTML
     // creates an extra <head> tag, and document.head.outerHTML creates
@@ -2112,48 +2160,148 @@ var WalkerActor = protocol.ActorClass({
       rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
       rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
     } else {
       rawNode.outerHTML = value;
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
-      value: Arg(1),
+      value: Arg(1, "string"),
     },
     response: {}
   }),
 
   /**
+   * Insert adjacent HTML to a node.
+   *
+   * @param {Node} node
+   * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
+   *                          "afterEnd" (see Element.insertAdjacentHTML).
+   * @param {string} value The HTML content.
+   */
+  insertAdjacentHTML: method(function(node, position, value) {
+    let rawNode = node.rawNode;
+    // Don't insert anything adjacent to the document element,
+    // the head or the body.
+    if (node.isDocumentElement()) {
+      throw new Error("Can't insert adjacent element to the root.");
+    }
+
+    let isInsertAsSibling = position === "beforeBegin" ||
+      position === "afterEnd";
+    if ((rawNode.tagName === "BODY" || rawNode.tagName === "HEAD") &&
+      isInsertAsSibling) {
+      throw new Error("Can't insert element before or after the body " +
+        "or the head.");
+    }
+
+    let rawParentNode = rawNode.parentNode;
+    if (!rawParentNode && isInsertAsSibling) {
+      throw new Error("Can't insert as sibling without parent node.");
+    }
+
+    // We can't use insertAdjacentHTML, because we want to return the nodes
+    // being created (so the front can remove them if the user undoes
+    // the change). So instead, use Range.createContextualFragment().
+    let range = rawNode.ownerDocument.createRange();
+    if (position === "beforeBegin" || position === "afterEnd") {
+      range.selectNode(rawNode);
+    } else {
+      range.selectNodeContents(rawNode);
+    }
+    let docFrag = range.createContextualFragment(value);
+    let newRawNodes = Array.from(docFrag.childNodes);
+    switch (position) {
+      case "beforeBegin":
+        rawParentNode.insertBefore(docFrag, rawNode);
+        break;
+      case "afterEnd":
+        // Note: if the second argument is null, rawParentNode.insertBefore
+        // behaves like rawParentNode.appendChild.
+        rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
+      case "afterBegin":
+        rawNode.insertBefore(docFrag, rawNode.firstChild);
+        break;
+      case "beforeEnd":
+        rawNode.appendChild(docFrag);
+        break;
+      default:
+        throw new Error('Invalid position value. Must be either ' +
+          '"beforeBegin", "beforeEnd", "afterBegin" or "afterEnd".');
+    }
+
+    return this.attachElements(newRawNodes);
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      position: Arg(1, "string"),
+      value: Arg(2, "string")
+    },
+    response: RetVal("disconnectedNodeArray")
+  }),
+
+  /**
+   * Test whether a node is a document or a document element.
+   *
+   * @param {NodeActor} node The node to remove.
+   * @return {boolean} True if the node is a document or a document element.
+   */
+  isDocumentOrDocumentElementNode: function(node) {
+      return ((node.rawNode.ownerDocument &&
+        node.rawNode.ownerDocument.documentElement === this.rawNode) ||
+        node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
+  },
+
+  /**
    * Removes a node from its parent node.
    *
+   * @param {NodeActor} node The node to remove.
    * @returns The node's nextSibling before it was removed.
    */
   removeNode: method(function(node) {
-    if ((node.rawNode.ownerDocument &&
-         node.rawNode.ownerDocument.documentElement === this.rawNode) ||
-         node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
+    if (this.isDocumentOrDocumentElementNode(node))
       throw Error("Cannot remove document or document elements.");
-    }
     let nextSibling = this.nextSibling(node);
-    if (node.rawNode.parentNode) {
-      node.rawNode.parentNode.removeChild(node.rawNode);
-      // Mutation events will take care of the rest.
-    }
+    node.rawNode.remove();
+    // Mutation events will take care of the rest.
     return nextSibling;
   }, {
     request: {
       node: Arg(0, "domnode")
     },
     response: {
       nextSibling: RetVal("nullable:domnode")
     }
   }),
 
   /**
+   * Removes an array of nodes from their parent node.
+   *
+   * @param {NodeActor[]} nodes The nodes to remove.
+   */
+  removeNodes: method(function(nodes) {
+    // Check that all nodes are valid before processing the removals.
+    for (let node of nodes) {
+      if (this.isDocumentOrDocumentElementNode(node))
+        throw Error("Cannot remove document or document elements.");
+    }
+
+    for (let node of nodes) {
+      node.rawNode.remove();
+      // Mutation events will take care of the rest.
+    }
+  }, {
+    request: {
+      node: Arg(0, "array:domnode")
+    },
+    response: {}
+  }),
+
+  /**
    * Insert a node into the DOM.
    */
   insertBefore: method(function(node, parent, sibling) {
     parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null);
   }, {
     request: {
       node: Arg(0, "domnode"),
       parent: Arg(1, "domnode"),
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -114,17 +114,21 @@ function RootActor(aConnection, aParamet
 }
 
 RootActor.prototype = {
   constructor: RootActor,
   applicationType: "browser",
 
   traits: {
     sources: true,
+    // Whether the inspector actor allows modifying outer HTML.
     editOuterHTML: true,
+    // Whether the inspector actor allows modifying innerHTML and inserting
+    // adjacent HTML.
+    pasteHTML: true,
     // Whether the server-side highlighter actor exists and can be used to
     // remotely highlight nodes (see server/actors/highlighter.js)
     highlightable: true,
     // Which custom highlighter does the server-side highlighter actor supports?
     // (see server/actors/highlighter.js)
     customHighlighters: [
       "BoxModelHighlighter",
       "CssTransformHighlighter",