Bug 1452566 - Release: devtools-reps 0.23.0 bundle; r=me. draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 17 Apr 2018 15:16:44 +0200
changeset 785449 9c0f8fe684f5a23f02cb8139969dd145e2c30152
parent 785448 ec3dd3ee2ae4b3a63529a912816a110e925eb2d0
child 785595 2491f4b792554a0c672a3ec0df3fbbabb0cc248e
child 785611 97eb543a9baa78a834c500fe8a8d1f71fe294a36
child 785615 f40be7758d604cfeacf3129cfe82d2ed267b3631
child 785620 cda292d1700a22fb420b3aa2af95ccaefddfd6fc
push id107234
push userbmo:nchevobbe@mozilla.com
push dateFri, 20 Apr 2018 06:54:51 +0000
reviewersme
bugs1452566
milestone61.0a1
Bug 1452566 - Release: devtools-reps 0.23.0 bundle; r=me. Change disabledFocus to focusable as it changed in the ObjectInspector. MozReview-Commit-ID: CDHotb0d4sL
devtools/client/shared/components/reps/reps.css
devtools/client/shared/components/reps/reps.js
devtools/client/webconsole/utils/object-inspector.js
--- a/devtools/client/shared/components/reps/reps.css
+++ b/devtools/client/shared/components/reps/reps.css
@@ -397,8 +397,18 @@ html[dir="rtl"] .tree-node img.arrow {
 .object-inspector .object-delimiter {
   color: var(--theme-comment);
 }
 
 .object-inspector .tree-node img.arrow {
   display: inline-block;
   vertical-align: middle;
 }
+
+/* Focused styles */
+.tree.object-inspector .tree-node.focused * {
+  color: inherit;
+}
+
+.tree-node.focused button.jump-definition,
+.tree-node.focused button.open-inspector {
+  background-color: currentColor;
+}
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1,18 +1,18 @@
 (function webpackUniversalModuleDefinition(root, factory) {
 	if(typeof exports === 'object' && typeof module === 'object')
-		module.exports = factory(require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/lodash"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux"));
+		module.exports = factory(require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux"));
 	else if(typeof define === 'function' && define.amd)
-		define(["devtools/client/shared/vendor/react-dom-factories", "devtools/client/shared/vendor/lodash", "devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react", "devtools/client/shared/vendor/react-redux", "devtools/client/shared/vendor/redux"], factory);
+		define(["devtools/client/shared/vendor/react-dom-factories", "devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react", "devtools/client/shared/vendor/react-redux", "devtools/client/shared/vendor/redux"], factory);
 	else {
-		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/lodash"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux")) : factory(root["devtools/client/shared/vendor/react-dom-factories"], root["devtools/client/shared/vendor/lodash"], root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react"], root["devtools/client/shared/vendor/react-redux"], root["devtools/client/shared/vendor/redux"]);
+		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react"), require("devtools/client/shared/vendor/react-redux"), require("devtools/client/shared/vendor/redux")) : factory(root["devtools/client/shared/vendor/react-dom-factories"], root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react"], root["devtools/client/shared/vendor/react-redux"], root["devtools/client/shared/vendor/redux"]);
 		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
 	}
-})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_54__, __WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_10__, __WEBPACK_EXTERNAL_MODULE_18__, __WEBPACK_EXTERNAL_MODULE_19__) {
+})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_9__, __WEBPACK_EXTERNAL_MODULE_18__, __WEBPACK_EXTERNAL_MODULE_19__) {
 return /******/ (function(modules) { // webpackBootstrap
 /******/ 	// The module cache
 /******/ 	var installedModules = {};
 /******/
 /******/ 	// The require function
 /******/ 	function __webpack_require__(moduleId) {
 /******/
 /******/ 		// Check if module is in cache
@@ -80,17 +80,17 @@ return /******/ (function(modules) { // 
 "use strict";
 
 
 /* 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/. */
 
 // Dependencies
-const validProtocols = /^(http|https|ftp|data|resource|chrome):/i;
+const validProtocols = /(http|https|ftp|data|resource|chrome):/i;
 const tokenSplitRegex = /(\s|\'|\"|\\)+/;
 const ELLIPSIS = "\u2026";
 const dom = __webpack_require__(1);
 const { span } = dom;
 
 /**
  * Returns true if the given object is a grip (see RDP protocol)
  */
@@ -427,22 +427,22 @@ function getGripType(object, noGrip) {
  * Determines whether a grip is a string containing a URL.
  *
  * @param string grip
  *        The grip, which may contain a URL.
  * @return boolean
  *         Whether the grip is a string containing a URL.
  */
 function containsURL(grip) {
-  if (typeof grip !== "string") {
+  // An URL can't be shorter than 5 char (e.g. "ftp:").
+  if (typeof grip !== "string" || grip.length < 5) {
     return false;
   }
 
-  let tokens = grip.split(tokenSplitRegex);
-  return tokens.some(isURL);
+  return validProtocols.test(grip);
 }
 
 /**
  * Determines whether a string token is a valid URL.
  *
  * @param string token
  *        The token.
  * @return boolean
@@ -562,17 +562,17 @@ const NaNRep = __webpack_require__(31);
 const Accessor = __webpack_require__(32);
 
 // DOM types (grips)
 const Attribute = __webpack_require__(33);
 const DateTime = __webpack_require__(34);
 const Document = __webpack_require__(35);
 const DocumentType = __webpack_require__(36);
 const Event = __webpack_require__(37);
-const Func = __webpack_require__(12);
+const Func = __webpack_require__(11);
 const PromiseRep = __webpack_require__(38);
 const RegExp = __webpack_require__(39);
 const StyleSheet = __webpack_require__(40);
 const CommentNode = __webpack_require__(41);
 const ElementNode = __webpack_require__(42);
 const TextNode = __webpack_require__(43);
 const ErrorRep = __webpack_require__(13);
 const Window = __webpack_require__(44);
@@ -584,17 +584,17 @@ const GripMapEntry = __webpack_require__
 const Grip = __webpack_require__(8);
 
 // List of all registered template.
 // XXX there should be a way for extensions to register a new
 // or modify an existing rep.
 let reps = [RegExp, StyleSheet, Event, DateTime, CommentNode, ElementNode, TextNode, Attribute, Func, PromiseRep, ArrayRep, Document, DocumentType, Window, ObjectWithText, ObjectWithURL, ErrorRep, GripArray, GripMap, GripMapEntry, Grip, Undefined, Null, StringRep, Number, SymbolRep, InfinityRep, NaNRep, Accessor];
 
 /**
- * Generic rep that is using for rendering native JS types or an object.
+ * Generic rep that is used for rendering native JS types or an object.
  * The right template used for rendering is picked automatically according
  * to the current value type. The value must be passed is as 'object'
  * property.
  */
 const Rep = function (props) {
   let {
     object,
     defaultRep
@@ -608,17 +608,17 @@ const Rep = function (props) {
 /**
  * Return a rep object that is responsible for rendering given
  * object.
  *
  * @param object {Object} Object to be rendered in the UI. This
  * can be generic JS object as well as a grip (handle to a remote
  * debuggee object).
  *
- * @param defaultObject {React.Component} The default template
+ * @param defaultRep {React.Component} The default template
  * that should be used to render given object if none is found.
  *
  * @param noGrip {Boolean} If true, will only check reps not made for remote objects.
  */
 function getRep(object, defaultRep = Obj, noGrip = false) {
   for (let i = 0; i < reps.length; i++) {
     let rep = reps[i];
     try {
@@ -708,17 +708,17 @@ const { a, span } = dom;
 /**
  * Renders a string. String value is enclosed within quotes.
  */
 StringRep.propTypes = {
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   style: PropTypes.object,
   cropLimit: PropTypes.number.isRequired,
-  member: PropTypes.string,
+  member: PropTypes.object,
   object: PropTypes.object.isRequired,
   openLink: PropTypes.func,
   className: PropTypes.string
 };
 
 function StringRep(props) {
   let {
     className,
@@ -729,23 +729,29 @@ function StringRep(props) {
     escapeWhitespace = true,
     member,
     openLink
   } = props;
 
   let text = object;
 
   const isLong = isLongString(object);
-  const shouldCrop = (!member || !member.open) && cropLimit && text.length > cropLimit;
+  const isOpen = member && member.open;
+  const shouldCrop = !isOpen && cropLimit && text.length > cropLimit;
 
   if (isLong) {
     text = maybeCropLongString({
       shouldCrop,
       cropLimit
     }, text);
+
+    const { fullText } = object;
+    if (isOpen && fullText) {
+      text = fullText;
+    }
   }
 
   text = formatText({
     useQuotes,
     escapeWhitespace
   }, text);
 
   const config = getElementConfig({
@@ -772,22 +778,21 @@ function StringRep(props) {
 
 function maybeCropLongString(opts, text) {
   const {
     shouldCrop,
     cropLimit
   } = opts;
 
   const {
-    fullText,
     initial,
     length
   } = text;
 
-  text = shouldCrop ? initial.substring(0, cropLimit) : fullText || initial;
+  text = shouldCrop ? initial.substring(0, cropLimit) : initial;
 
   if (text.length < length) {
     text += ELLIPSIS;
   }
 
   return text;
 }
 
@@ -1502,70 +1507,36 @@ let Grip = {
   maxLengthMap
 };
 
 // Exports from this module
 module.exports = Grip;
 
 /***/ }),
 /* 9 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/. */
-
-module.exports = {
-  ELEMENT_NODE: 1,
-  ATTRIBUTE_NODE: 2,
-  TEXT_NODE: 3,
-  CDATA_SECTION_NODE: 4,
-  ENTITY_REFERENCE_NODE: 5,
-  ENTITY_NODE: 6,
-  PROCESSING_INSTRUCTION_NODE: 7,
-  COMMENT_NODE: 8,
-  DOCUMENT_NODE: 9,
-  DOCUMENT_TYPE_NODE: 10,
-  DOCUMENT_FRAGMENT_NODE: 11,
-  NOTATION_NODE: 12,
-
-  // DocumentPosition
-  DOCUMENT_POSITION_DISCONNECTED: 0x01,
-  DOCUMENT_POSITION_PRECEDING: 0x02,
-  DOCUMENT_POSITION_FOLLOWING: 0x04,
-  DOCUMENT_POSITION_CONTAINS: 0x08,
-  DOCUMENT_POSITION_CONTAINED_BY: 0x10,
-  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20
-};
+/***/ (function(module, exports) {
+
+module.exports = __WEBPACK_EXTERNAL_MODULE_9__;
 
 /***/ }),
 /* 10 */
-/***/ (function(module, exports) {
-
-module.exports = __WEBPACK_EXTERNAL_MODULE_10__;
-
-/***/ }),
-/* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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 client = __webpack_require__(20);
 const loadProperties = __webpack_require__(21);
 const node = __webpack_require__(22);
 const { nodeIsError, nodeIsPrimitive } = node;
-const selection = __webpack_require__(55);
+const selection = __webpack_require__(54);
 
 const { MODE } = __webpack_require__(3);
 const {
   REPS: {
     Rep,
     Grip
   }
 } = __webpack_require__(4);
@@ -1595,17 +1566,17 @@ module.exports = {
   loadProperties,
   node,
   renderRep,
   selection,
   shouldRenderRootsInReps
 };
 
 /***/ }),
-/* 12 */
+/* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/. */
@@ -1767,16 +1738,50 @@ module.exports = {
   rep: wrapRender(FunctionRep),
   supportsObject,
   cleanFunctionName,
   // exported for testing purpose.
   getFunctionName
 };
 
 /***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* 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/. */
+
+module.exports = {
+  ELEMENT_NODE: 1,
+  ATTRIBUTE_NODE: 2,
+  TEXT_NODE: 3,
+  CDATA_SECTION_NODE: 4,
+  ENTITY_REFERENCE_NODE: 5,
+  ENTITY_NODE: 6,
+  PROCESSING_INSTRUCTION_NODE: 7,
+  COMMENT_NODE: 8,
+  DOCUMENT_NODE: 9,
+  DOCUMENT_TYPE_NODE: 10,
+  DOCUMENT_FRAGMENT_NODE: 11,
+  NOTATION_NODE: 12,
+
+  // DocumentPosition
+  DOCUMENT_POSITION_DISCONNECTED: 0x01,
+  DOCUMENT_POSITION_PRECEDING: 0x02,
+  DOCUMENT_POSITION_FOLLOWING: 0x04,
+  DOCUMENT_POSITION_CONTAINS: 0x08,
+  DOCUMENT_POSITION_CONTAINED_BY: 0x10,
+  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 0x20
+};
+
+/***/ }),
 /* 13 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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
@@ -1785,17 +1790,17 @@ module.exports = {
 // ReactJS
 const PropTypes = __webpack_require__(2);
 // Utils
 const {
   getGripType,
   isGrip,
   wrapRender
 } = __webpack_require__(0);
-const { cleanFunctionName } = __webpack_require__(12);
+const { cleanFunctionName } = __webpack_require__(11);
 const { isLongString } = __webpack_require__(5);
 const { MODE } = __webpack_require__(3);
 
 const dom = __webpack_require__(1);
 const { span } = dom;
 const IGNORED_SOURCE_URLS = ["debugger eval code"];
 
 /**
@@ -2489,17 +2494,17 @@ function GripMapEntry(props) {
     suppressQuotes: false
   }));
 }
 
 function supportsObject(grip, noGrip = false) {
   if (noGrip === true) {
     return false;
   }
-  return grip && grip.type === "mapEntry" && grip.preview;
+  return grip && (grip.type === "mapEntry" || grip.type === "storageEntry") && grip.preview;
 }
 
 function createGripMapEntry(key, value) {
   return {
     type: "mapEntry",
     preview: {
       key,
       value
@@ -2582,28 +2587,51 @@ async function enumSymbols(objectClient,
 async function getPrototype(objectClient) {
   if (typeof objectClient.getPrototype !== "function") {
     console.error("objectClient.getPrototype is not a function");
     return Promise.resolve({});
   }
   return objectClient.getPrototype();
 }
 
+async function getFullText(longStringClient, object) {
+  const { initial, length } = object;
+
+  return new Promise((resolve, reject) => {
+    longStringClient.substring(initial.length, length, response => {
+      if (response.error) {
+        console.error("LongStringClient.substring", response.error + ": " + response.message);
+        reject({});
+        return;
+      }
+
+      resolve({
+        fullText: initial + response.substring
+      });
+    });
+  });
+}
+
 function iteratorSlice(iterator, start, end) {
   start = start || 0;
   const count = end ? end - start + 1 : iterator.count;
+
+  if (count === 0) {
+    return Promise.resolve({});
+  }
   return iterator.slice(start, count);
 }
 
 module.exports = {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   enumSymbols,
-  getPrototype
+  getPrototype,
+  getFullText
 };
 
 /***/ }),
 /* 21 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -2612,36 +2640,38 @@ module.exports = {
  * 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 {
   enumEntries,
   enumIndexedProperties,
   enumNonIndexedProperties,
   getPrototype,
-  enumSymbols
+  enumSymbols,
+  getFullText
 } = __webpack_require__(20);
 
 const {
   getClosestGripNode,
   getClosestNonBucketNode,
   getValue,
   nodeHasAccessors,
   nodeHasAllEntriesInPreview,
   nodeHasProperties,
   nodeIsBucket,
   nodeIsDefaultProperties,
   nodeIsEntries,
   nodeIsMapEntry,
   nodeIsPrimitive,
   nodeIsProxy,
-  nodeNeedsNumericalBuckets
+  nodeNeedsNumericalBuckets,
+  nodeIsLongString
 } = __webpack_require__(22);
 
-function loadItemProperties(item, createObjectClient, loadedProperties) {
+function loadItemProperties(item, createObjectClient, createLongStringClient, loadedProperties) {
   const gripItem = getClosestGripNode(item);
   const value = getValue(gripItem);
 
   const [start, end] = item.meta ? [item.meta.startIndex, item.meta.endIndex] : [];
 
   let promises = [];
   let objectClient;
   const getObjectClient = () => objectClient || createObjectClient(value);
@@ -2661,16 +2691,20 @@ function loadItemProperties(item, create
   if (shouldLoadItemPrototype(item, loadedProperties)) {
     promises.push(getPrototype(getObjectClient()));
   }
 
   if (shouldLoadItemSymbols(item, loadedProperties)) {
     promises.push(enumSymbols(getObjectClient(), start, end));
   }
 
+  if (shouldLoadItemFullText(item, loadedProperties)) {
+    promises.push(getFullText(createLongStringClient(value), value));
+  }
+
   return Promise.all(promises).then(mergeResponses);
 }
 
 function mergeResponses(responses) {
   const data = {};
 
   for (const response of responses) {
     if (response.hasOwnProperty("ownProperties")) {
@@ -2679,16 +2713,20 @@ function mergeResponses(responses) {
 
     if (response.ownSymbols && response.ownSymbols.length > 0) {
       data.ownSymbols = response.ownSymbols;
     }
 
     if (response.prototype) {
       data.prototype = response.prototype;
     }
+
+    if (response.fullText) {
+      data.fullText = response.fullText;
+    }
   }
 
   return data;
 }
 
 function shouldLoadItemIndexedProperties(item, loadedProperties = new Map()) {
   const gripItem = getClosestGripNode(item);
   const value = getValue(gripItem);
@@ -2712,53 +2750,58 @@ function shouldLoadItemEntries(item, loa
   const value = getValue(gripItem);
 
   return value && nodeIsEntries(getClosestNonBucketNode(item)) && !nodeHasAllEntriesInPreview(gripItem) && !loadedProperties.has(item.path) && !nodeNeedsNumericalBuckets(item);
 }
 
 function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item);
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
 }
 
 function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
   const value = getValue(item);
 
-  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsProxy(item);
+  return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
+}
+
+function shouldLoadItemFullText(item, loadedProperties = new Map()) {
+  return !loadedProperties.has(item.path) && nodeIsLongString(item);
 }
 
 module.exports = {
   loadItemProperties,
   mergeResponses,
   shouldLoadItemEntries,
   shouldLoadItemIndexedProperties,
   shouldLoadItemNonIndexedProperties,
   shouldLoadItemPrototype,
-  shouldLoadItemSymbols
+  shouldLoadItemSymbols,
+  shouldLoadItemFullText
 };
 
 /***/ }),
 /* 22 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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 { get, has } = __webpack_require__(54);
 const { maybeEscapePropertyName } = __webpack_require__(0);
 const ArrayRep = __webpack_require__(6);
 const GripArrayRep = __webpack_require__(14);
 const GripMap = __webpack_require__(16);
 const GripMapEntryRep = __webpack_require__(17);
 const ErrorRep = __webpack_require__(13);
+const { isLongString } = __webpack_require__(5);
 
 const MAX_NUMERICAL_PROPERTIES = 100;
 
 const NODE_TYPES = {
   BUCKET: Symbol("[n…m]"),
   DEFAULT_PROPERTIES: Symbol("<default properties>"),
   ENTRIES: Symbol("<entries>"),
   GET: Symbol("<get>"),
@@ -2781,22 +2824,22 @@ if (typeof window === "object") {
   WINDOW_PROPERTIES = Object.getOwnPropertyNames(window);
 }
 
 function getType(item) {
   return item.type;
 }
 
 function getValue(item) {
-  if (has(item, "contents.value")) {
-    return get(item, "contents.value");
-  }
-
-  if (has(item, "contents.getterValue")) {
-    return get(item, "contents.getterValue", undefined);
+  if (item && item.contents && item.contents.hasOwnProperty("value")) {
+    return item.contents.value;
+  }
+
+  if (item && item.contents && item.contents.hasOwnProperty("getterValue")) {
+    return item.contents.getterValue;
   }
 
   if (nodeHasAccessors(item)) {
     return item.contents;
   }
 
   return undefined;
 }
@@ -2863,17 +2906,17 @@ function nodeIsMissingArguments(item) {
   return !nodeHasChildren(item) && value && value.missingArguments;
 }
 
 function nodeHasProperties(item) {
   return !nodeHasChildren(item) && nodeIsObject(item);
 }
 
 function nodeIsPrimitive(item) {
-  return !nodeHasChildren(item) && !nodeHasProperties(item) && !nodeIsEntries(item) && !nodeIsMapEntry(item) && !nodeHasAccessors(item) && !nodeIsBucket(item);
+  return !nodeHasChildren(item) && !nodeHasProperties(item) && !nodeIsEntries(item) && !nodeIsMapEntry(item) && !nodeHasAccessors(item) && !nodeIsBucket(item) && !nodeIsLongString(item);
 }
 
 function nodeIsDefaultProperties(item) {
   return getType(item) === NODE_TYPES.DEFAULT_PROPERTIES;
 }
 
 function isDefaultWindowProperty(name) {
   return WINDOW_PROPERTIES.includes(name);
@@ -2921,33 +2964,42 @@ function nodeIsSetter(item) {
 function nodeIsBlock(item) {
   return getType(item) === NODE_TYPES.BLOCK;
 }
 
 function nodeIsError(item) {
   return ErrorRep.supportsObject(getValue(item));
 }
 
+function nodeIsLongString(item) {
+  return isLongString(getValue(item));
+}
+
+function nodeHasFullText(item) {
+  const value = getValue(item);
+  return nodeIsLongString(item) && value.hasOwnProperty("fullText");
+}
+
 function nodeHasAccessors(item) {
   return !!getNodeGetter(item) || !!getNodeSetter(item);
 }
 
 function nodeSupportsNumericalBucketing(item) {
   // We exclude elements with entries since it's the <entries> node
   // itself that can have buckets.
   return nodeIsArrayLike(item) && !nodeHasEntries(item) || nodeIsEntries(item) || nodeIsBucket(item);
 }
 
 function nodeHasEntries(item) {
   const value = getValue(item);
   if (!value) {
     return false;
   }
 
-  return value.class === "Map" || value.class === "Set" || value.class === "WeakMap" || value.class === "WeakSet";
+  return value.class === "Map" || value.class === "Set" || value.class === "WeakMap" || value.class === "WeakSet" || value.class === "Storage";
 }
 
 function nodeHasAllEntriesInPreview(item) {
   const { preview } = getValue(item) || {};
   if (!preview) {
     return false;
   }
 
@@ -3081,21 +3133,21 @@ function makeNodesForMapEntry(item) {
     parent: item,
     name: "<value>",
     contents: { value },
     type: NODE_TYPES.MAP_ENTRY_VALUE
   })];
 }
 
 function getNodeGetter(item) {
-  return get(item, "contents.get", undefined);
+  return item && item.contents ? item.contents.get : undefined;
 }
 
 function getNodeSetter(item) {
-  return get(item, "contents.set", undefined);
+  return item && item.contents ? item.contents.set : undefined;
 }
 
 function makeNodesForAccessors(item) {
   const accessors = [];
 
   const getter = getNodeGetter(item);
   if (getter && getter.type !== "undefined") {
     accessors.push(createNode({
@@ -3255,16 +3307,28 @@ function makeNodesForProperties(objProps
   // Add the prototype if it exists and is not null
   if (prototype && prototype.type !== "null") {
     nodes.push(makeNodeForPrototype(objProps, parent));
   }
 
   return nodes;
 }
 
+function setNodeFullText(loadedProps, node) {
+  if (nodeHasFullText(node)) {
+    return node;
+  }
+
+  if (nodeIsLongString(node)) {
+    node.contents.value.fullText = loadedProps.fullText;
+  }
+
+  return node;
+}
+
 function makeNodeForPrototype(objProps, parent) {
   const {
     prototype
   } = objProps || {};
 
   // Add the prototype if it exists and is not null
   if (prototype && prototype.type !== "null") {
     return createNode({
@@ -3360,19 +3424,23 @@ function getChildren(options) {
   if (nodeIsMapEntry(item)) {
     return addToCache(makeNodesForMapEntry(item));
   }
 
   if (nodeIsProxy(item)) {
     return addToCache(makeNodesForProxyProperties(item));
   }
 
+  if (nodeIsLongString(item) && hasLoadedProps) {
+    // Set longString object's fullText to fetched one.
+    return addToCache(setNodeFullText(loadedProps, item));
+  }
+
   if (nodeNeedsNumericalBuckets(item) && hasLoadedProps) {
     // Even if we have numerical buckets, we should have loaded non indexed properties,
-    // like length for example.
     const bucketNodes = makeNumericalBuckets(item);
     return addToCache(bucketNodes.concat(makeNodesForProperties(loadedProps, item)));
   }
 
   if (!nodeIsEntries(item) && !nodeIsBucket(item) && !nodeHasProperties(item)) {
     return [];
   }
 
@@ -3458,16 +3526,18 @@ module.exports = {
   nodeHasChildren,
   nodeHasEntries,
   nodeHasProperties,
   nodeIsBlock,
   nodeIsBucket,
   nodeIsDefaultProperties,
   nodeIsEntries,
   nodeIsError,
+  nodeIsLongString,
+  nodeHasFullText,
   nodeIsFunction,
   nodeIsGetter,
   nodeIsMapEntry,
   nodeIsMissingArguments,
   nodeIsObject,
   nodeIsOptimizedOut,
   nodeIsPrimitive,
   nodeIsPromise,
@@ -3494,17 +3564,17 @@ module.exports = {
 
 /* 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 { MODE } = __webpack_require__(3);
 const { REPS, getRep } = __webpack_require__(4);
 const ObjectInspector = __webpack_require__(47);
-const ObjectInspectorUtils = __webpack_require__(11);
+const ObjectInspectorUtils = __webpack_require__(10);
 
 const {
   parseURLEncodedText,
   parseURLParams,
   maybeEscapePropertyName,
   getGripPreviewItems
 } = __webpack_require__(0);
 
@@ -4652,17 +4722,17 @@ module.exports = {
 const PropTypes = __webpack_require__(2);
 const {
   isGrip,
   cropString,
   cropMultipleLines,
   wrapRender
 } = __webpack_require__(0);
 const { MODE } = __webpack_require__(3);
-const nodeConstants = __webpack_require__(9);
+const nodeConstants = __webpack_require__(12);
 const dom = __webpack_require__(1);
 const { span } = dom;
 
 /**
  * Renders DOM comment node.
  */
 CommentNode.propTypes = {
   object: PropTypes.object.isRequired,
@@ -4719,92 +4789,118 @@ const PropTypes = __webpack_require__(2)
 
 // Utils
 const {
   isGrip,
   wrapRender
 } = __webpack_require__(0);
 const { rep: StringRep } = __webpack_require__(5);
 const { MODE } = __webpack_require__(3);
-const nodeConstants = __webpack_require__(9);
+const nodeConstants = __webpack_require__(12);
 
 const dom = __webpack_require__(1);
 const { span } = dom;
 
 /**
  * Renders DOM element node.
  */
 ElementNode.propTypes = {
   object: PropTypes.object.isRequired,
+  inspectIconTitle: PropTypes.string,
   // @TODO Change this to Object.values once it's supported in Node's version of V8
   mode: PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
+  onDOMNodeClick: PropTypes.func,
   onDOMNodeMouseOver: PropTypes.func,
   onDOMNodeMouseOut: PropTypes.func,
   onInspectIconClick: PropTypes.func
 };
 
 function ElementNode(props) {
   let {
     object,
+    inspectIconTitle,
     mode,
+    onDOMNodeClick,
     onDOMNodeMouseOver,
     onDOMNodeMouseOut,
     onInspectIconClick
   } = props;
   let elements = getElements(object, mode);
 
   let isInTree = object.preview && object.preview.isConnected === true;
 
   let baseConfig = {
     "data-link-actor-id": object.actor,
     className: "objectBox objectBox-node"
   };
   let inspectIcon;
   if (isInTree) {
+    if (onDOMNodeClick) {
+      Object.assign(baseConfig, {
+        onClick: _ => onDOMNodeClick(object)
+      });
+    }
+
     if (onDOMNodeMouseOver) {
       Object.assign(baseConfig, {
         onMouseOver: _ => onDOMNodeMouseOver(object)
       });
     }
 
     if (onDOMNodeMouseOut) {
       Object.assign(baseConfig, {
         onMouseOut: onDOMNodeMouseOut
       });
     }
 
     if (onInspectIconClick) {
       inspectIcon = dom.button({
         className: "open-inspector",
         // TODO: Localize this with "openNodeInInspector" when Bug 1317038 lands
-        title: "Click to select the node in the inspector",
-        onClick: e => onInspectIconClick(object, e)
+        title: inspectIconTitle || "Click to select the node in the inspector",
+        onClick: e => {
+          if (onDOMNodeClick) {
+            e.stopPropagation();
+          }
+
+          onInspectIconClick(object, e);
+        }
       });
     }
   }
 
   return span(baseConfig, ...elements, inspectIcon);
 }
 
 function getElements(grip, mode) {
-  let { attributes, nodeName } = grip.preview;
+  let {
+    attributes,
+    nodeName,
+    isAfterPseudoElement,
+    isBeforePseudoElement
+  } = grip.preview;
   const nodeNameElement = span({
     className: "tag-name"
   }, nodeName);
 
+  if (isAfterPseudoElement || isBeforePseudoElement) {
+    return [span({ className: "attrName" }, `::${isAfterPseudoElement ? "after" : "before"}`)];
+  }
+
   if (mode === MODE.TINY) {
     let elements = [nodeNameElement];
     if (attributes.id) {
       elements.push(span({ className: "attrName" }, `#${attributes.id}`));
     }
     if (attributes.class) {
       elements.push(span({ className: "attrName" }, attributes.class.trim().split(/\s+/).map(cls => `.${cls}`).join("")));
     }
     return elements;
   }
+
   let attributeKeys = Object.keys(attributes);
   if (attributeKeys.includes("class")) {
     attributeKeys.splice(attributeKeys.indexOf("class"), 1);
     attributeKeys.unshift("class");
   }
   if (attributeKeys.includes("id")) {
     attributeKeys.splice(attributeKeys.indexOf("id"), 1);
     attributeKeys.unshift("id");
@@ -5159,21 +5255,21 @@ module.exports = {
 
 "use strict";
 
 
 /* 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 { createElement, createFactory, PureComponent } = __webpack_require__(10);
+const { createElement, createFactory, PureComponent } = __webpack_require__(9);
 const { Provider } = __webpack_require__(18);
 const ObjectInspector = createFactory(__webpack_require__(48));
-const createStore = __webpack_require__(57);
-const Utils = __webpack_require__(11);
+const createStore = __webpack_require__(56);
+const Utils = __webpack_require__(10);
 const {
   renderRep,
   shouldRenderRootsInReps
 } = Utils;
 
 class OI extends PureComponent {
 
   constructor(props) {
@@ -5213,30 +5309,30 @@ function _interopRequireDefault(obj) { r
 
 /* 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 {
   Component,
   createFactory
-} = __webpack_require__(10);
+} = __webpack_require__(9);
 const dom = __webpack_require__(1);
 const { connect } = __webpack_require__(18);
 const { bindActionCreators } = __webpack_require__(19);
 
 const Tree = createFactory(_devtoolsComponents2.default.Tree);
 __webpack_require__(52);
 
 const classnames = __webpack_require__(53);
 const {
   MODE
 } = __webpack_require__(3);
 
-const Utils = __webpack_require__(11);
+const Utils = __webpack_require__(10);
 
 const {
   getChildren,
   getClosestGripNode,
   getParent,
   getValue,
   nodeHasAccessors,
   nodeHasProperties,
@@ -5248,17 +5344,19 @@ const {
   nodeIsMissingArguments,
   nodeIsOptimizedOut,
   nodeIsPrimitive,
   nodeIsPrototype,
   nodeIsSetter,
   nodeIsUninitializedBinding,
   nodeIsUnmappedBinding,
   nodeIsUnscopedBinding,
-  nodeIsWindow
+  nodeIsWindow,
+  nodeIsLongString,
+  nodeHasFullText
 } = Utils.node;
 
 // This implements a component that renders an interactive inspector
 // for looking at JavaScript objects. It expects descriptions of
 // objects from the protocol, and will dynamically fetch children
 // properties as objects are expanded.
 //
 // If you want to inspect a single object, pass the name and the
@@ -5295,32 +5393,54 @@ class ObjectInspector extends Component 
     self.setExpanded = this.setExpanded.bind(this);
     self.focusItem = this.focusItem.bind(this);
     self.getRoots = this.getRoots.bind(this);
   }
 
   shouldComponentUpdate(nextProps) {
     const {
       expandedPaths,
+      focusedItem,
       loadedProperties,
       roots
     } = this.props;
 
     if (roots !== nextProps.roots) {
-      // Since the roots changed, we assume the properties did as well. Thus we can clear
-      // the cachedNodes to avoid bugs and memory leaks.
+      // Since the roots changed, we assume the properties did as well, so we need to
+      // cleanup the component internal state.
+
+      // We can clear the cachedNodes to avoid bugs and memory leaks.
       this.cachedNodes.clear();
+      // The rootsChanged action will be handled in a middleware to release the actors
+      // of the old roots, as well as cleanup the state properties (expandedPaths,
+      // loadedProperties, …).
+      this.props.rootsChanged(nextProps);
+      // We don't render right away since the state is going to be changed by the
+      // rootsChanged action. The `state.forceUpdate` flag will be set to `true` so we
+      // can execute a new render cycle with the cleaned state.
+      return false;
+    }
+
+    if (nextProps.forceUpdate === true) {
       return true;
     }
 
     // We should update if:
     // - there are new loaded properties
     // - OR the expanded paths number changed, and all of them have properties loaded
     // - OR the expanded paths number did not changed, but old and new sets differ
-    return loadedProperties.size !== nextProps.loadedProperties.size || expandedPaths.size !== nextProps.expandedPaths.size && [...nextProps.expandedPaths].every(path => nextProps.loadedProperties.has(path)) || expandedPaths.size === nextProps.expandedPaths.size && [...nextProps.expandedPaths].some(key => !expandedPaths.has(key));
+    // - OR the focused node changed.
+    return loadedProperties.size !== nextProps.loadedProperties.size || expandedPaths.size !== nextProps.expandedPaths.size && [...nextProps.expandedPaths].every(path => nextProps.loadedProperties.has(path)) || expandedPaths.size === nextProps.expandedPaths.size && [...nextProps.expandedPaths].some(key => !expandedPaths.has(key)) || focusedItem !== nextProps.focusedItem;
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.forceUpdate) {
+      // If the component was updated, we can then reset the forceUpdate flag.
+      this.props.forceUpdated();
+    }
   }
 
   componentWillUnmount() {
     const { releaseActor } = this.props;
     if (typeof releaseActor !== "function") {
       return;
     }
 
@@ -5353,49 +5473,55 @@ class ObjectInspector extends Component 
 
   setExpanded(item, expand) {
     if (nodeIsPrimitive(item)) {
       return;
     }
 
     const {
       createObjectClient,
+      createLongStringClient,
       loadedProperties,
       nodeExpand,
       nodeCollapse,
       roots
     } = this.props;
 
     if (expand === true) {
       const gripItem = getClosestGripNode(item);
       const value = getValue(gripItem);
       const isRoot = value && roots.some(root => {
         const rootValue = getValue(root);
         return rootValue && rootValue.actor === value.actor;
       });
       const actor = isRoot || !value ? null : value.actor;
-      nodeExpand(item, actor, loadedProperties, createObjectClient);
+      nodeExpand(item, actor, loadedProperties, createObjectClient, createLongStringClient);
     } else {
       nodeCollapse(item);
     }
   }
 
   focusItem(item) {
     const {
+      focusable = true,
       focusedItem,
+      nodeFocus,
       onFocus
     } = this.props;
 
-    if (focusedItem !== item && onFocus) {
-      onFocus(item);
+    if (focusable && focusedItem !== item) {
+      nodeFocus(item);
+      if (focusedItem !== item && onFocus) {
+        onFocus(item);
+      }
     }
   }
 
   getTreeItemLabelAndValue(item, depth, expanded) {
-    let label = item.name;
+    const label = item.name;
     const isPrimitive = nodeIsPrimitive(item);
 
     if (nodeIsOptimizedOut(item)) {
       return {
         label,
         value: dom.span({ className: "unavailable" }, "(optimized away)")
       };
     }
@@ -5442,28 +5568,34 @@ class ObjectInspector extends Component 
       return {
         label: Utils.renderRep(item, {
           ...this.props,
           functionName: label
         })
       };
     }
 
-    if (nodeHasProperties(item) || nodeHasAccessors(item) || nodeIsMapEntry(item) || isPrimitive) {
-      let repsProp = { ...this.props };
+    if (nodeHasProperties(item) || nodeHasAccessors(item) || nodeIsMapEntry(item) || nodeIsLongString(item) || isPrimitive) {
+      let repProps = { ...this.props };
       if (depth > 0) {
-        repsProp.mode = this.props.mode === MODE.LONG ? MODE.SHORT : MODE.TINY;
+        repProps.mode = this.props.mode === MODE.LONG ? MODE.SHORT : MODE.TINY;
       }
       if (expanded) {
-        repsProp.mode = MODE.TINY;
+        repProps.mode = MODE.TINY;
+      }
+
+      if (nodeIsLongString(item)) {
+        repProps.member = {
+          open: nodeHasFullText(item) && expanded
+        };
       }
 
       return {
         label,
-        value: Utils.renderRep(item, repsProp)
+        value: Utils.renderRep(item, repProps)
       };
     }
 
     return {
       label
     };
   }
 
@@ -5493,35 +5625,48 @@ class ObjectInspector extends Component 
           setExpanded: this.setExpanded
         });
       } : undefined
     }, label);
   }
 
   getTreeTopElementProps(item, depth, focused, expanded) {
     const {
+      onCmdCtrlClick,
       onDoubleClick,
       dimTopLevelWindow
     } = this.props;
 
     let parentElementProps = {
       className: classnames("node object-node", {
         focused,
         lessen: !expanded && (nodeIsDefaultProperties(item) || nodeIsPrototype(item) || dimTopLevelWindow === true && nodeIsWindow(item) && depth === 0),
         block: nodeIsBlock(item)
       }),
       onClick: e => {
-        e.stopPropagation();
-
-        // If the user selected text, bail out.
-        if (Utils.selection.documentHasSelection()) {
+        if (e.metaKey && onCmdCtrlClick) {
+          onCmdCtrlClick(item, {
+            depth,
+            event: e,
+            focused,
+            expanded
+          });
+          e.stopPropagation();
           return;
         }
 
-        this.setExpanded(item, !expanded);
+        // If this click happened because the user selected some text, bail out.
+        // Note that if the user selected some text before and then clicks here,
+        // the previously selected text will be first unselected, unless the user
+        // clicked on the arrow itself. Indeed because the arrow is an image, clicking on
+        // it does not remove any existing text selection. So we need to also check if
+        // teh arrow was clicked.
+        if (Utils.selection.documentHasSelection() && !(e.target && e.target.matches && e.target.matches(".arrow"))) {
+          e.stopPropagation();
+        }
       }
     };
 
     if (onDoubleClick) {
       parentElementProps.onDoubleClick = e => {
         e.stopPropagation();
         onDoubleClick(item, {
           depth,
@@ -5541,62 +5686,63 @@ class ObjectInspector extends Component 
 
     return dom.div(this.getTreeTopElementProps(item, depth, focused, expanded), arrow, labelElement, delimiter, value);
   }
 
   render() {
     const {
       autoExpandAll = true,
       autoExpandDepth = 1,
-      disabledFocus,
+      focusable = true,
       disableWrap = false,
       expandedPaths,
       focusedItem,
       inline
     } = this.props;
 
     return Tree({
       className: classnames({
         inline,
         nowrap: disableWrap,
         "object-inspector": true
       }),
       autoExpandAll,
       autoExpandDepth,
-      disabledFocus,
 
       isExpanded: item => expandedPaths && expandedPaths.has(item.path),
       isExpandable: item => nodeIsPrimitive(item) === false,
       focused: focusedItem,
 
       getRoots: this.getRoots,
       getParent,
       getChildren: this.getItemChildren,
       getKey: this.getNodeKey,
 
       onExpand: item => this.setExpanded(item, true),
       onCollapse: item => this.setExpanded(item, false),
-      onFocus: this.focusItem,
+      onFocus: focusable ? this.focusItem : null,
 
       renderItem: this.renderTreeItem
     });
   }
 }
 
 function mapStateToProps(state, props) {
   return {
     actors: state.actors,
     expandedPaths: state.expandedPaths,
-    focusedItem: state.focusedItem,
-    loadedProperties: state.loadedProperties
+    // If the root changes, we want to pass a possibly new focusedItem property
+    focusedItem: state.roots !== props.roots ? props.focusedItem : state.focusedItem,
+    loadedProperties: state.loadedProperties,
+    forceUpdate: state.forceUpdate
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators(__webpack_require__(56), dispatch);
+  return bindActionCreators(__webpack_require__(55), dispatch);
 }
 
 module.exports = connect(mapStateToProps, mapDispatchToProps)(ObjectInspector);
 
 /***/ }),
 /* 49 */
 /***/ (function(module, exports, __webpack_require__) {
 
@@ -5621,17 +5767,17 @@ module.exports = {
 
 "use strict";
 
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
-var _react = __webpack_require__(10);
+var _react = __webpack_require__(9);
 
 var _react2 = _interopRequireDefault(_react);
 
 var _reactDomFactories = __webpack_require__(1);
 
 var _reactDomFactories2 = _interopRequireDefault(_reactDomFactories);
 
 var _propTypes = __webpack_require__(2);
@@ -5955,16 +6101,19 @@ class Tree extends Component {
       //     onExpand(item: Item)
       //     onCollapse(item: Item)
       //
       // Example:
       //
       //     onExpand: item => dispatchExpandActionToRedux(item)
       onExpand: _propTypes2.default.func,
       onCollapse: _propTypes2.default.func,
+      // Optional event handler called with the current focused node when the Enter key
+      // is pressed. Can be useful to allow further keyboard actions within the tree node.
+      onActivate: _propTypes2.default.func,
       isExpandable: _propTypes2.default.func,
       // Additional classes to add to the root element.
       className: _propTypes2.default.string,
       // style object to be applied to the root element.
       style: _propTypes2.default.object
     };
   }
 
@@ -5982,42 +6131,49 @@ class Tree extends Component {
       seen: new Set()
     };
 
     this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
     this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
     this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
     this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
     this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(this);
+    this._focusFirstNode = oncePerAnimationFrame(this._focusFirstNode).bind(this);
+    this._focusLastNode = oncePerAnimationFrame(this._focusLastNode).bind(this);
 
     this._autoExpand = this._autoExpand.bind(this);
     this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
     this._dfs = this._dfs.bind(this);
     this._dfsFromRoots = this._dfsFromRoots.bind(this);
     this._focus = this._focus.bind(this);
     this._scrollNodeIntoView = this._scrollNodeIntoView.bind(this);
     this._onBlur = this._onBlur.bind(this);
     this._onKeyDown = this._onKeyDown.bind(this);
     this._nodeIsExpandable = this._nodeIsExpandable.bind(this);
+    this._activateNode = oncePerAnimationFrame(this._activateNode).bind(this);
   }
 
   componentDidMount() {
     this._autoExpand();
     if (this.props.focused) {
       this._scrollNodeIntoView(this.props.focused);
+      // Always keep the focus on the tree itself.
+      this.treeRef.focus();
     }
   }
 
   componentWillReceiveProps(nextProps) {
     this._autoExpand();
   }
 
   componentDidUpdate(prevProps, prevState) {
     if (prevProps.focused !== this.props.focused) {
       this._scrollNodeIntoView(this.props.focused);
+      // Always keep the focus on the tree itself.
+      this.treeRef.focus();
     }
   }
 
   _autoExpand() {
     if (!this.props.autoExpandDepth) {
       return;
     }
 
@@ -6147,18 +6303,21 @@ class Tree extends Component {
    *        The item to be focused, or undefined to focus no item.
    *
    * @param {Object|undefined} options
    *        An options object which can contain:
    *          - dir: "up" or "down" to indicate if we should scroll the element to the
    *                 top or the bottom of the scrollable container when the element is
    *                 off canvas.
    */
-  _focus(item, options) {
-    this._scrollNodeIntoView(item, options);
+  _focus(item, options = {}) {
+    const { preventAutoScroll } = options;
+    if (item && !preventAutoScroll) {
+      this._scrollNodeIntoView(item, options);
+    }
     if (this.props.onFocus) {
       this.props.onFocus(item);
     }
   }
 
   /**
    * Sets the passed in item to be the focused item.
    *
@@ -6170,33 +6329,36 @@ class Tree extends Component {
    *          - dir: "up" or "down" to indicate if we should scroll the element to the
    *                 top or the bottom of the scrollable container when the element is
    *                 off canvas.
    */
   _scrollNodeIntoView(item, options = {}) {
     if (item !== undefined) {
       const treeElement = this.treeRef;
       const element = document.getElementById(this.props.getKey(item));
+
       if (element) {
         const { top, bottom } = element.getBoundingClientRect();
         const closestScrolledParent = node => {
           if (node == null) {
             return null;
           }
 
           if (node.scrollHeight > node.clientHeight) {
             return node;
           }
           return closestScrolledParent(node.parentNode);
         };
         const scrolledParent = closestScrolledParent(treeElement);
-        const isVisible = !scrolledParent || top >= 0 && bottom <= scrolledParent.clientHeight;
+        const scrolledParentRect = scrolledParent ? scrolledParent.getBoundingClientRect() : null;
+        const isVisible = !scrolledParent || top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom;
 
         if (!isVisible) {
-          let scrollToTop = !options.alignTo && top < 0 || options.alignTo === "top";
+          const { alignTo } = options;
+          let scrollToTop = alignTo ? alignTo === "top" : !scrolledParentRect || top < scrolledParentRect.top;
           element.scrollIntoView(scrollToTop);
         }
       }
     }
   }
 
   /**
    * Sets the state to have no focused item.
@@ -6240,16 +6402,28 @@ class Tree extends Component {
         return;
 
       case "ArrowRight":
         if (this._nodeIsExpandable(this.props.focused) && !this.props.isExpanded(this.props.focused)) {
           this._onExpand(this.props.focused);
         } else {
           this._focusNextNode();
         }
+        return;
+
+      case "Home":
+        this._focusFirstNode();
+        return;
+
+      case "End":
+        this._focusLastNode();
+        return;
+
+      case "Enter":
+        this._activateNode();
     }
   }
 
   /**
    * Sets the previous node relative to the currently focused item, to focused.
    */
   _focusPrevNode() {
     // Start a depth first search and keep going until we reach the currently
@@ -6316,16 +6490,33 @@ class Tree extends Component {
       if (traversal[parentIndex].item === parent) {
         break;
       }
     }
 
     this._focus(parent, { alignTo: "top" });
   }
 
+  _focusFirstNode() {
+    const traversal = this._dfsFromRoots();
+    this._focus(traversal[0].item, { alignTo: "top" });
+  }
+
+  _focusLastNode() {
+    const traversal = this._dfsFromRoots();
+    const lastIndex = traversal.length - 1;
+    this._focus(traversal[lastIndex].item, { alignTo: "bottom" });
+  }
+
+  _activateNode() {
+    if (this.props.onActivate) {
+      this.props.onActivate(this.props.focused);
+    }
+  }
+
   _nodeIsExpandable(item) {
     return this.props.isExpandable ? this.props.isExpandable(item) : !!this.props.getChildren(item).length;
   }
 
   render() {
     const traversal = this._dfsFromRoots();
     const {
       focused
@@ -6342,17 +6533,19 @@ class Tree extends Component {
         depth,
         renderItem: this.props.renderItem,
         focused: focused === item,
         expanded: this.props.isExpanded(item),
         isExpandable: this._nodeIsExpandable(item),
         onExpand: this._onExpand,
         onCollapse: this._onCollapse,
         onClick: e => {
-          this._focus(item);
+          // Since the user just clicked the node, there's no need to check if it should
+          // be scrolled into view.
+          this._focus(item, { preventAutoScroll: true });
           if (this.props.isExpanded(item)) {
             this.props.onCollapse(item);
           } else {
             this.props.onExpand(item, e.altKey);
           }
         }
       });
     });
@@ -6464,84 +6657,64 @@ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBP
 	} else {
 		window.classNames = classNames;
 	}
 }());
 
 
 /***/ }),
 /* 54 */
-/***/ (function(module, exports) {
-
-module.exports = __WEBPACK_EXTERNAL_MODULE_54__;
-
-/***/ }),
-/* 55 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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 { ELEMENT_NODE } = __webpack_require__(9);
-
 function documentHasSelection() {
   const selection = getSelection();
   if (!selection) {
     return false;
   }
 
-  const {
-    anchorNode,
-    focusNode
-  } = selection;
-
-  // When clicking the arrow, which is an inline svg element, the selection do have a type
-  // of "Range". We need to have an explicit case when the anchor and the focus node are
-  // the same and they have an arrow ancestor.
-  if (focusNode && focusNode === anchorNode && focusNode.nodeType == ELEMENT_NODE && focusNode.closest(".arrow")) {
-    return false;
-  }
-
   return selection.type === "Range";
 }
 
 module.exports = {
   documentHasSelection
 };
 
 /***/ }),
-/* 56 */
+/* 55 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 const {
   loadItemProperties
 } = __webpack_require__(21); /* This Source Code Form is subject to the terms of the Mozilla Public
                                          * License, v. 2.0. If a copy of the MPL was not distributed with this
                                          * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * This action is responsible for expanding a given node,
  * which also means that it will call the action responsible to fetch properties.
  */
-function nodeExpand(node, actor, loadedProperties, createObjectClient) {
+function nodeExpand(node, actor, loadedProperties, createObjectClient, createLongStringClient) {
   return async ({ dispatch }) => {
     dispatch({
       type: "NODE_EXPAND",
       data: { node }
     });
 
     if (!loadedProperties.has(node.path)) {
-      dispatch(nodeLoadProperties(node, actor, loadedProperties, createObjectClient));
+      dispatch(nodeLoadProperties(node, actor, loadedProperties, createObjectClient, createLongStringClient));
     }
   };
 }
 
 function nodeCollapse(node) {
   return {
     type: "NODE_COLLAPSE",
     data: { node }
@@ -6553,79 +6726,130 @@ function nodeFocus(node) {
     type: "NODE_FOCUS",
     data: { node }
   };
 }
 /*
  * This action checks if we need to fetch properties, entries, prototype and symbols
  * for a given node. If we do, it will call the appropriate ObjectClient functions.
  */
-function nodeLoadProperties(item, actor, loadedProperties, createObjectClient) {
+function nodeLoadProperties(item, actor, loadedProperties, createObjectClient, createLongStringClient) {
   return async ({ dispatch }) => {
     try {
-      const properties = await loadItemProperties(item, createObjectClient, loadedProperties);
+      const properties = await loadItemProperties(item, createObjectClient, createLongStringClient, loadedProperties);
       dispatch(nodePropertiesLoaded(item, actor, properties));
     } catch (e) {
       console.error(e);
     }
   };
 }
 
 function nodePropertiesLoaded(node, actor, properties) {
   return {
     type: "NODE_PROPERTIES_LOADED",
     data: { node, actor, properties }
   };
 }
 
+/*
+ * This action is dispatched when the `roots` prop, provided by a consumer of the
+ * ObjectInspector (inspector, console, …), is modified. It will clean the internal
+ * state properties (expandedPaths, loadedProperties, …) and release the actors consumed
+ * with the previous roots.
+ * It takes a props argument which reflects what is passed by the upper-level consumer.
+ */
+function rootsChanged(props) {
+  return {
+    type: "ROOTS_CHANGED",
+    data: props
+  };
+}
+
+/*
+ * This action will reset the `forceUpdate` flag in the state.
+ */
+function forceUpdated() {
+  return {
+    type: "FORCE_UPDATED"
+  };
+}
+
 module.exports = {
+  forceUpdated,
   nodeExpand,
   nodeCollapse,
   nodeFocus,
   nodeLoadProperties,
-  nodePropertiesLoaded
+  nodePropertiesLoaded,
+  rootsChanged
 };
 
 /***/ }),
-/* 57 */
+/* 56 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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 { applyMiddleware, createStore } = __webpack_require__(19);
-const { thunk } = __webpack_require__(58);
-const { waitUntilService } = __webpack_require__(59);
-const reducer = __webpack_require__(60);
+const { applyMiddleware, createStore, compose } = __webpack_require__(19);
+const { thunk } = __webpack_require__(57);
+const { waitUntilService } = __webpack_require__(58);
+const reducer = __webpack_require__(59);
 
 function createInitialState(overrides) {
   return {
     actors: new Set(),
     expandedPaths: new Set(),
     focusedItem: null,
     loadedProperties: new Map(),
+    forceUpdated: false,
     ...overrides
   };
 }
 
+function enableStateReinitializer(props) {
+  return next => (innerReducer, initialState, enhancer) => {
+    function reinitializerEnhancer(state, action) {
+      if (action.type !== "ROOTS_CHANGED") {
+        return innerReducer(state, action);
+      }
+
+      if (props.releaseActor && initialState.actors) {
+        initialState.actors.forEach(props.releaseActor);
+      }
+
+      return {
+        ...action.data,
+        actors: new Set(),
+        expandedPaths: new Set(),
+        loadedProperties: new Map(),
+        // Indicates to the component that we do want to render on the next render cycle.
+        forceUpdate: true
+      };
+    }
+    return next(reinitializerEnhancer, initialState, enhancer);
+  };
+}
+
 module.exports = props => {
   const middlewares = [thunk];
+
   if (props.injectWaitService) {
     middlewares.push(waitUntilService);
   }
 
-  return createStore(reducer, createInitialState(props), applyMiddleware(...middlewares));
+  return createStore(reducer, createInitialState(props), compose(applyMiddleware(...middlewares), enableStateReinitializer(props)));
 };
 
 /***/ }),
-/* 58 */
+/* 57 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/. */
@@ -6639,17 +6863,17 @@ module.exports = props => {
 function thunk({ dispatch, getState }) {
   return next => action => {
     return typeof action === "function" ? action({ dispatch, getState }) : next(action);
   };
 }
 exports.thunk = thunk;
 
 /***/ }),
-/* 59 */
+/* 58 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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/. */
@@ -6713,17 +6937,17 @@ function waitUntilService({ dispatch, ge
 }
 
 module.exports = {
   WAIT_UNTIL_TYPE,
   waitUntilService
 };
 
 /***/ }),
-/* 60 */
+/* 59 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 function reducer(state = {}, action) {
   const {
     type,
@@ -6747,21 +6971,31 @@ function reducer(state = {}, action) {
   if (type === "NODE_PROPERTIES_LOADED") {
     return cloneState({
       actors: data.actor ? new Set(state.actors || []).add(data.actor) : state.actors,
       loadedProperties: new Map(state.loadedProperties).set(data.node.path, action.data.properties)
     });
   }
 
   if (type === "NODE_FOCUS") {
+    if (state.focusedItem === data.node) {
+      return state;
+    }
+
     return cloneState({
       focusedItem: data.node
     });
   }
 
+  if (type === "FORCE_UPDATED") {
+    return cloneState({
+      forceUpdate: false
+    });
+  }
+
   return state;
 } /* 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/. */
 
 
 module.exports = reducer;
 
--- a/devtools/client/webconsole/utils/object-inspector.js
+++ b/devtools/client/webconsole/utils/object-inspector.js
@@ -43,17 +43,17 @@ function getObjectInspector(grip, servic
       : null;
   }
 
   const objectInspectorProps = {
     autoExpandDepth: 0,
     mode: MODE.LONG,
     // TODO: we disable focus since it's not currently working well in ObjectInspector.
     // Let's remove the property below when problem are fixed in OI.
-    disabledFocus: true,
+    focusable: false,
     roots: [{
       path: (grip && grip.actor) || JSON.stringify(grip),
       contents: {
         value: grip
       }
     }],
     createObjectClient: object =>
       new ObjectClient(serviceContainer.hudProxy.client, object),