Bug 1452566 - Release: devtools-reps 0.23.0 bundle; r=nchevobbe.
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 17 Apr 2018 15:16:44 +0200
changeset 468174 75cf0521f6f3e93bf81e1de228ff5b5de1e942cb
parent 468173 c2bc44fd23ad6f6f309b5ac6ff6a467ad459869e
child 468175 fdb7b761a45a2fa3f154126cb5418f9dcd0e29bd
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1452566
milestone61.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 1452566 - Release: devtools-reps 0.23.0 bundle; r=nchevobbe. 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),