1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. // This context combines tree/selection state, search, and the owners stack.
    
  11. // These values are managed together because changes in one often impact the others.
    
  12. // Combining them enables us to avoid cascading renders.
    
  13. //
    
  14. // Changes to search state may impact tree state.
    
  15. // For example, updating the selected search result also updates the tree's selected value.
    
  16. // Search does not fundamentally change the tree though.
    
  17. // It is also possible to update the selected tree value independently.
    
  18. //
    
  19. // Changes to owners state mask search and tree values.
    
  20. // When owners stack is not empty, search is temporarily disabled,
    
  21. // and tree values (e.g. num elements, selected element) are masked.
    
  22. // Both tree and search values are restored when the owners stack is cleared.
    
  23. //
    
  24. // For this reason, changes to the tree context are processed in sequence: tree -> search -> owners
    
  25. // This enables each section to potentially override (or mask) previous values.
    
  26. 
    
  27. import type {ReactContext} from 'shared/ReactTypes';
    
  28. 
    
  29. import * as React from 'react';
    
  30. import {
    
  31.   createContext,
    
  32.   useCallback,
    
  33.   useContext,
    
  34.   useEffect,
    
  35.   useLayoutEffect,
    
  36.   useMemo,
    
  37.   useReducer,
    
  38.   useRef,
    
  39.   startTransition,
    
  40. } from 'react';
    
  41. import {createRegExp} from '../utils';
    
  42. import {BridgeContext, StoreContext} from '../context';
    
  43. import Store from '../../store';
    
  44. 
    
  45. import type {Element} from 'react-devtools-shared/src/frontend/types';
    
  46. 
    
  47. export type StateContext = {
    
  48.   // Tree
    
  49.   numElements: number,
    
  50.   ownerSubtreeLeafElementID: number | null,
    
  51.   selectedElementID: number | null,
    
  52.   selectedElementIndex: number | null,
    
  53. 
    
  54.   // Search
    
  55.   searchIndex: number | null,
    
  56.   searchResults: Array<number>,
    
  57.   searchText: string,
    
  58. 
    
  59.   // Owners
    
  60.   ownerID: number | null,
    
  61.   ownerFlatTree: Array<Element> | null,
    
  62. 
    
  63.   // Inspection element panel
    
  64.   inspectedElementID: number | null,
    
  65. };
    
  66. 
    
  67. type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
    
  68.   type: 'GO_TO_NEXT_SEARCH_RESULT',
    
  69. };
    
  70. type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {
    
  71.   type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
    
  72. };
    
  73. type ACTION_HANDLE_STORE_MUTATION = {
    
  74.   type: 'HANDLE_STORE_MUTATION',
    
  75.   payload: [Array<number>, Map<number, number>],
    
  76. };
    
  77. type ACTION_RESET_OWNER_STACK = {
    
  78.   type: 'RESET_OWNER_STACK',
    
  79. };
    
  80. type ACTION_SELECT_CHILD_ELEMENT_IN_TREE = {
    
  81.   type: 'SELECT_CHILD_ELEMENT_IN_TREE',
    
  82. };
    
  83. type ACTION_SELECT_ELEMENT_AT_INDEX = {
    
  84.   type: 'SELECT_ELEMENT_AT_INDEX',
    
  85.   payload: number | null,
    
  86. };
    
  87. type ACTION_SELECT_ELEMENT_BY_ID = {
    
  88.   type: 'SELECT_ELEMENT_BY_ID',
    
  89.   payload: number | null,
    
  90. };
    
  91. type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {
    
  92.   type: 'SELECT_NEXT_ELEMENT_IN_TREE',
    
  93. };
    
  94. type ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {
    
  95.   type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
    
  96. };
    
  97. type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {
    
  98.   type: 'SELECT_NEXT_SIBLING_IN_TREE',
    
  99. };
    
  100. type ACTION_SELECT_OWNER = {
    
  101.   type: 'SELECT_OWNER',
    
  102.   payload: number,
    
  103. };
    
  104. type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {
    
  105.   type: 'SELECT_PARENT_ELEMENT_IN_TREE',
    
  106. };
    
  107. type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {
    
  108.   type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
    
  109. };
    
  110. type ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {
    
  111.   type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
    
  112. };
    
  113. type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {
    
  114.   type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
    
  115. };
    
  116. type ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE = {
    
  117.   type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE',
    
  118. };
    
  119. type ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE = {
    
  120.   type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE',
    
  121. };
    
  122. type ACTION_SET_SEARCH_TEXT = {
    
  123.   type: 'SET_SEARCH_TEXT',
    
  124.   payload: string,
    
  125. };
    
  126. type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {
    
  127.   type: 'UPDATE_INSPECTED_ELEMENT_ID',
    
  128. };
    
  129. 
    
  130. type Action =
    
  131.   | ACTION_GO_TO_NEXT_SEARCH_RESULT
    
  132.   | ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
    
  133.   | ACTION_HANDLE_STORE_MUTATION
    
  134.   | ACTION_RESET_OWNER_STACK
    
  135.   | ACTION_SELECT_CHILD_ELEMENT_IN_TREE
    
  136.   | ACTION_SELECT_ELEMENT_AT_INDEX
    
  137.   | ACTION_SELECT_ELEMENT_BY_ID
    
  138.   | ACTION_SELECT_NEXT_ELEMENT_IN_TREE
    
  139.   | ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
    
  140.   | ACTION_SELECT_NEXT_SIBLING_IN_TREE
    
  141.   | ACTION_SELECT_OWNER
    
  142.   | ACTION_SELECT_PARENT_ELEMENT_IN_TREE
    
  143.   | ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
    
  144.   | ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
    
  145.   | ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
    
  146.   | ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
    
  147.   | ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
    
  148.   | ACTION_SET_SEARCH_TEXT
    
  149.   | ACTION_UPDATE_INSPECTED_ELEMENT_ID;
    
  150. 
    
  151. export type DispatcherContext = (action: Action) => void;
    
  152. 
    
  153. const TreeStateContext: ReactContext<StateContext> =
    
  154.   createContext<StateContext>(((null: any): StateContext));
    
  155. TreeStateContext.displayName = 'TreeStateContext';
    
  156. 
    
  157. const TreeDispatcherContext: ReactContext<DispatcherContext> =
    
  158.   createContext<DispatcherContext>(((null: any): DispatcherContext));
    
  159. TreeDispatcherContext.displayName = 'TreeDispatcherContext';
    
  160. 
    
  161. type State = {
    
  162.   // Tree
    
  163.   numElements: number,
    
  164.   ownerSubtreeLeafElementID: number | null,
    
  165.   selectedElementID: number | null,
    
  166.   selectedElementIndex: number | null,
    
  167. 
    
  168.   // Search
    
  169.   searchIndex: number | null,
    
  170.   searchResults: Array<number>,
    
  171.   searchText: string,
    
  172. 
    
  173.   // Owners
    
  174.   ownerID: number | null,
    
  175.   ownerFlatTree: Array<Element> | null,
    
  176. 
    
  177.   // Inspection element panel
    
  178.   inspectedElementID: number | null,
    
  179. };
    
  180. 
    
  181. function reduceTreeState(store: Store, state: State, action: Action): State {
    
  182.   let {
    
  183.     numElements,
    
  184.     ownerSubtreeLeafElementID,
    
  185.     selectedElementIndex,
    
  186.     selectedElementID,
    
  187.   } = state;
    
  188.   const ownerID = state.ownerID;
    
  189. 
    
  190.   let lookupIDForIndex = true;
    
  191. 
    
  192.   // Base tree should ignore selected element changes when the owner's tree is active.
    
  193.   if (ownerID === null) {
    
  194.     switch (action.type) {
    
  195.       case 'HANDLE_STORE_MUTATION':
    
  196.         numElements = store.numElements;
    
  197. 
    
  198.         // If the currently-selected Element has been removed from the tree, update selection state.
    
  199.         const removedIDs = action.payload[1];
    
  200.         // Find the closest parent that wasn't removed during this batch.
    
  201.         // We deduce the parent-child mapping from removedIDs (id -> parentID)
    
  202.         // because by now it's too late to read them from the store.
    
  203.         while (
    
  204.           selectedElementID !== null &&
    
  205.           removedIDs.has(selectedElementID)
    
  206.         ) {
    
  207.           selectedElementID = ((removedIDs.get(
    
  208.             selectedElementID,
    
  209.           ): any): number);
    
  210.         }
    
  211.         if (selectedElementID === 0) {
    
  212.           // The whole root was removed.
    
  213.           selectedElementIndex = null;
    
  214.         }
    
  215.         break;
    
  216.       case 'SELECT_CHILD_ELEMENT_IN_TREE':
    
  217.         ownerSubtreeLeafElementID = null;
    
  218. 
    
  219.         if (selectedElementIndex !== null) {
    
  220.           const selectedElement = store.getElementAtIndex(
    
  221.             ((selectedElementIndex: any): number),
    
  222.           );
    
  223.           if (
    
  224.             selectedElement !== null &&
    
  225.             selectedElement.children.length > 0 &&
    
  226.             !selectedElement.isCollapsed
    
  227.           ) {
    
  228.             const firstChildID = selectedElement.children[0];
    
  229.             const firstChildIndex = store.getIndexOfElementID(firstChildID);
    
  230.             if (firstChildIndex !== null) {
    
  231.               selectedElementIndex = firstChildIndex;
    
  232.             }
    
  233.           }
    
  234.         }
    
  235.         break;
    
  236.       case 'SELECT_ELEMENT_AT_INDEX':
    
  237.         ownerSubtreeLeafElementID = null;
    
  238. 
    
  239.         selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
    
  240.         break;
    
  241.       case 'SELECT_ELEMENT_BY_ID':
    
  242.         ownerSubtreeLeafElementID = null;
    
  243. 
    
  244.         // Skip lookup in this case; it would be redundant.
    
  245.         // It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
    
  246.         lookupIDForIndex = false;
    
  247. 
    
  248.         selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
    
  249.         selectedElementIndex =
    
  250.           selectedElementID === null
    
  251.             ? null
    
  252.             : store.getIndexOfElementID(selectedElementID);
    
  253.         break;
    
  254.       case 'SELECT_NEXT_ELEMENT_IN_TREE':
    
  255.         ownerSubtreeLeafElementID = null;
    
  256. 
    
  257.         if (
    
  258.           selectedElementIndex === null ||
    
  259.           selectedElementIndex + 1 >= numElements
    
  260.         ) {
    
  261.           selectedElementIndex = 0;
    
  262.         } else {
    
  263.           selectedElementIndex++;
    
  264.         }
    
  265.         break;
    
  266.       case 'SELECT_NEXT_SIBLING_IN_TREE':
    
  267.         ownerSubtreeLeafElementID = null;
    
  268. 
    
  269.         if (selectedElementIndex !== null) {
    
  270.           const selectedElement = store.getElementAtIndex(
    
  271.             ((selectedElementIndex: any): number),
    
  272.           );
    
  273.           if (selectedElement !== null && selectedElement.parentID !== 0) {
    
  274.             const parent = store.getElementByID(selectedElement.parentID);
    
  275.             if (parent !== null) {
    
  276.               const {children} = parent;
    
  277.               const selectedChildIndex = children.indexOf(selectedElement.id);
    
  278.               const nextChildID =
    
  279.                 selectedChildIndex < children.length - 1
    
  280.                   ? children[selectedChildIndex + 1]
    
  281.                   : children[0];
    
  282.               selectedElementIndex = store.getIndexOfElementID(nextChildID);
    
  283.             }
    
  284.           }
    
  285.         }
    
  286.         break;
    
  287.       case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
    
  288.         if (selectedElementIndex !== null) {
    
  289.           if (
    
  290.             ownerSubtreeLeafElementID !== null &&
    
  291.             ownerSubtreeLeafElementID !== selectedElementID
    
  292.           ) {
    
  293.             const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
    
  294.             if (leafElement !== null) {
    
  295.               let currentElement: null | Element = leafElement;
    
  296.               while (currentElement !== null) {
    
  297.                 if (currentElement.ownerID === selectedElementID) {
    
  298.                   selectedElementIndex = store.getIndexOfElementID(
    
  299.                     currentElement.id,
    
  300.                   );
    
  301.                   break;
    
  302.                 } else if (currentElement.ownerID !== 0) {
    
  303.                   currentElement = store.getElementByID(currentElement.ownerID);
    
  304.                 }
    
  305.               }
    
  306.             }
    
  307.           }
    
  308.         }
    
  309.         break;
    
  310.       case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
    
  311.         if (selectedElementIndex !== null) {
    
  312.           if (ownerSubtreeLeafElementID === null) {
    
  313.             // If this is the first time we're stepping through the owners tree,
    
  314.             // pin the current component as the owners list leaf.
    
  315.             // This will enable us to step back down to this component.
    
  316.             ownerSubtreeLeafElementID = selectedElementID;
    
  317.           }
    
  318. 
    
  319.           const selectedElement = store.getElementAtIndex(
    
  320.             ((selectedElementIndex: any): number),
    
  321.           );
    
  322.           if (selectedElement !== null && selectedElement.ownerID !== 0) {
    
  323.             const ownerIndex = store.getIndexOfElementID(
    
  324.               selectedElement.ownerID,
    
  325.             );
    
  326.             if (ownerIndex !== null) {
    
  327.               selectedElementIndex = ownerIndex;
    
  328.             }
    
  329.           }
    
  330.         }
    
  331.         break;
    
  332.       case 'SELECT_PARENT_ELEMENT_IN_TREE':
    
  333.         ownerSubtreeLeafElementID = null;
    
  334. 
    
  335.         if (selectedElementIndex !== null) {
    
  336.           const selectedElement = store.getElementAtIndex(
    
  337.             ((selectedElementIndex: any): number),
    
  338.           );
    
  339.           if (selectedElement !== null && selectedElement.parentID !== 0) {
    
  340.             const parentIndex = store.getIndexOfElementID(
    
  341.               selectedElement.parentID,
    
  342.             );
    
  343.             if (parentIndex !== null) {
    
  344.               selectedElementIndex = parentIndex;
    
  345.             }
    
  346.           }
    
  347.         }
    
  348.         break;
    
  349.       case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
    
  350.         ownerSubtreeLeafElementID = null;
    
  351. 
    
  352.         if (selectedElementIndex === null || selectedElementIndex === 0) {
    
  353.           selectedElementIndex = numElements - 1;
    
  354.         } else {
    
  355.           selectedElementIndex--;
    
  356.         }
    
  357.         break;
    
  358.       case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
    
  359.         ownerSubtreeLeafElementID = null;
    
  360. 
    
  361.         if (selectedElementIndex !== null) {
    
  362.           const selectedElement = store.getElementAtIndex(
    
  363.             ((selectedElementIndex: any): number),
    
  364.           );
    
  365.           if (selectedElement !== null && selectedElement.parentID !== 0) {
    
  366.             const parent = store.getElementByID(selectedElement.parentID);
    
  367.             if (parent !== null) {
    
  368.               const {children} = parent;
    
  369.               const selectedChildIndex = children.indexOf(selectedElement.id);
    
  370.               const nextChildID =
    
  371.                 selectedChildIndex > 0
    
  372.                   ? children[selectedChildIndex - 1]
    
  373.                   : children[children.length - 1];
    
  374.               selectedElementIndex = store.getIndexOfElementID(nextChildID);
    
  375.             }
    
  376.           }
    
  377.         }
    
  378.         break;
    
  379.       case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
    
  380.         const elementIndicesWithErrorsOrWarnings =
    
  381.           store.getElementsWithErrorsAndWarnings();
    
  382.         if (elementIndicesWithErrorsOrWarnings.length === 0) {
    
  383.           return state;
    
  384.         }
    
  385. 
    
  386.         let flatIndex = 0;
    
  387.         if (selectedElementIndex !== null) {
    
  388.           // Resume from the current position in the list.
    
  389.           // Otherwise step to the previous item, relative to the current selection.
    
  390.           for (
    
  391.             let i = elementIndicesWithErrorsOrWarnings.length - 1;
    
  392.             i >= 0;
    
  393.             i--
    
  394.           ) {
    
  395.             const {index} = elementIndicesWithErrorsOrWarnings[i];
    
  396.             if (index >= selectedElementIndex) {
    
  397.               flatIndex = i;
    
  398.             } else {
    
  399.               break;
    
  400.             }
    
  401.           }
    
  402.         }
    
  403. 
    
  404.         let prevEntry;
    
  405.         if (flatIndex === 0) {
    
  406.           prevEntry =
    
  407.             elementIndicesWithErrorsOrWarnings[
    
  408.               elementIndicesWithErrorsOrWarnings.length - 1
    
  409.             ];
    
  410.           selectedElementID = prevEntry.id;
    
  411.           selectedElementIndex = prevEntry.index;
    
  412.         } else {
    
  413.           prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
    
  414.           selectedElementID = prevEntry.id;
    
  415.           selectedElementIndex = prevEntry.index;
    
  416.         }
    
  417. 
    
  418.         lookupIDForIndex = false;
    
  419.         break;
    
  420.       }
    
  421.       case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
    
  422.         const elementIndicesWithErrorsOrWarnings =
    
  423.           store.getElementsWithErrorsAndWarnings();
    
  424.         if (elementIndicesWithErrorsOrWarnings.length === 0) {
    
  425.           return state;
    
  426.         }
    
  427. 
    
  428.         let flatIndex = -1;
    
  429.         if (selectedElementIndex !== null) {
    
  430.           // Resume from the current position in the list.
    
  431.           // Otherwise step to the next item, relative to the current selection.
    
  432.           for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
    
  433.             const {index} = elementIndicesWithErrorsOrWarnings[i];
    
  434.             if (index <= selectedElementIndex) {
    
  435.               flatIndex = i;
    
  436.             } else {
    
  437.               break;
    
  438.             }
    
  439.           }
    
  440.         }
    
  441. 
    
  442.         let nextEntry;
    
  443.         if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
    
  444.           nextEntry = elementIndicesWithErrorsOrWarnings[0];
    
  445.           selectedElementID = nextEntry.id;
    
  446.           selectedElementIndex = nextEntry.index;
    
  447.         } else {
    
  448.           nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
    
  449.           selectedElementID = nextEntry.id;
    
  450.           selectedElementIndex = nextEntry.index;
    
  451.         }
    
  452. 
    
  453.         lookupIDForIndex = false;
    
  454.         break;
    
  455.       }
    
  456.       default:
    
  457.         // React can bailout of no-op updates.
    
  458.         return state;
    
  459.     }
    
  460.   }
    
  461. 
    
  462.   // Keep selected item ID and index in sync.
    
  463.   if (lookupIDForIndex && selectedElementIndex !== state.selectedElementIndex) {
    
  464.     if (selectedElementIndex === null) {
    
  465.       selectedElementID = null;
    
  466.     } else {
    
  467.       selectedElementID = store.getElementIDAtIndex(
    
  468.         ((selectedElementIndex: any): number),
    
  469.       );
    
  470.     }
    
  471.   }
    
  472. 
    
  473.   return {
    
  474.     ...state,
    
  475. 
    
  476.     numElements,
    
  477.     ownerSubtreeLeafElementID,
    
  478.     selectedElementIndex,
    
  479.     selectedElementID,
    
  480.   };
    
  481. }
    
  482. 
    
  483. function reduceSearchState(store: Store, state: State, action: Action): State {
    
  484.   let {
    
  485.     searchIndex,
    
  486.     searchResults,
    
  487.     searchText,
    
  488.     selectedElementID,
    
  489.     selectedElementIndex,
    
  490.   } = state;
    
  491.   const ownerID = state.ownerID;
    
  492. 
    
  493.   const prevSearchIndex = searchIndex;
    
  494.   const prevSearchText = searchText;
    
  495.   const numPrevSearchResults = searchResults.length;
    
  496. 
    
  497.   // We track explicitly whether search was requested because
    
  498.   // we might want to search even if search index didn't change.
    
  499.   // For example, if you press "next result" on a search with a single
    
  500.   // result but a different current selection, we'll set this to true.
    
  501.   let didRequestSearch = false;
    
  502. 
    
  503.   // Search isn't supported when the owner's tree is active.
    
  504.   if (ownerID === null) {
    
  505.     switch (action.type) {
    
  506.       case 'GO_TO_NEXT_SEARCH_RESULT':
    
  507.         if (numPrevSearchResults > 0) {
    
  508.           didRequestSearch = true;
    
  509.           searchIndex =
    
  510.             // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
    
  511.             searchIndex + 1 < numPrevSearchResults ? searchIndex + 1 : 0;
    
  512.         }
    
  513.         break;
    
  514.       case 'GO_TO_PREVIOUS_SEARCH_RESULT':
    
  515.         if (numPrevSearchResults > 0) {
    
  516.           didRequestSearch = true;
    
  517.           searchIndex =
    
  518.             ((searchIndex: any): number) > 0
    
  519.               ? ((searchIndex: any): number) - 1
    
  520.               : numPrevSearchResults - 1;
    
  521.         }
    
  522.         break;
    
  523.       case 'HANDLE_STORE_MUTATION':
    
  524.         if (searchText !== '') {
    
  525.           const [addedElementIDs, removedElementIDs] =
    
  526.             (action: ACTION_HANDLE_STORE_MUTATION).payload;
    
  527. 
    
  528.           removedElementIDs.forEach((parentID, id) => {
    
  529.             // Prune this item from the search results.
    
  530.             const index = searchResults.indexOf(id);
    
  531.             if (index >= 0) {
    
  532.               searchResults = searchResults
    
  533.                 .slice(0, index)
    
  534.                 .concat(searchResults.slice(index + 1));
    
  535. 
    
  536.               // If the results are now empty, also deselect things.
    
  537.               if (searchResults.length === 0) {
    
  538.                 searchIndex = null;
    
  539.               } else if (((searchIndex: any): number) >= searchResults.length) {
    
  540.                 searchIndex = searchResults.length - 1;
    
  541.               }
    
  542.             }
    
  543.           });
    
  544. 
    
  545.           addedElementIDs.forEach(id => {
    
  546.             const element = ((store.getElementByID(id): any): Element);
    
  547. 
    
  548.             // It's possible that multiple tree operations will fire before this action has run.
    
  549.             // So it's important to check for elements that may have been added and then removed.
    
  550.             if (element !== null) {
    
  551.               const {displayName} = element;
    
  552. 
    
  553.               // Add this item to the search results if it matches.
    
  554.               const regExp = createRegExp(searchText);
    
  555.               if (displayName !== null && regExp.test(displayName)) {
    
  556.                 const newElementIndex = ((store.getIndexOfElementID(
    
  557.                   id,
    
  558.                 ): any): number);
    
  559. 
    
  560.                 let foundMatch = false;
    
  561.                 for (let index = 0; index < searchResults.length; index++) {
    
  562.                   const resultID = searchResults[index];
    
  563.                   if (
    
  564.                     newElementIndex <
    
  565.                     ((store.getIndexOfElementID(resultID): any): number)
    
  566.                   ) {
    
  567.                     foundMatch = true;
    
  568.                     searchResults = searchResults
    
  569.                       .slice(0, index)
    
  570.                       .concat(resultID)
    
  571.                       .concat(searchResults.slice(index));
    
  572.                     break;
    
  573.                   }
    
  574.                 }
    
  575.                 if (!foundMatch) {
    
  576.                   searchResults = searchResults.concat(id);
    
  577.                 }
    
  578. 
    
  579.                 searchIndex = searchIndex === null ? 0 : searchIndex;
    
  580.               }
    
  581.             }
    
  582.           });
    
  583.         }
    
  584.         break;
    
  585.       case 'SET_SEARCH_TEXT':
    
  586.         searchIndex = null;
    
  587.         searchResults = [];
    
  588.         searchText = (action: ACTION_SET_SEARCH_TEXT).payload;
    
  589. 
    
  590.         if (searchText !== '') {
    
  591.           const regExp = createRegExp(searchText);
    
  592.           store.roots.forEach(rootID => {
    
  593.             recursivelySearchTree(store, rootID, regExp, searchResults);
    
  594.           });
    
  595.           if (searchResults.length > 0) {
    
  596.             if (prevSearchIndex === null) {
    
  597.               if (selectedElementIndex !== null) {
    
  598.                 searchIndex = getNearestResultIndex(
    
  599.                   store,
    
  600.                   searchResults,
    
  601.                   selectedElementIndex,
    
  602.                 );
    
  603.               } else {
    
  604.                 searchIndex = 0;
    
  605.               }
    
  606.             } else {
    
  607.               searchIndex = Math.min(
    
  608.                 ((prevSearchIndex: any): number),
    
  609.                 searchResults.length - 1,
    
  610.               );
    
  611.             }
    
  612.           }
    
  613.         }
    
  614.         break;
    
  615.       default:
    
  616.         // React can bailout of no-op updates.
    
  617.         return state;
    
  618.     }
    
  619.   }
    
  620. 
    
  621.   if (searchText !== prevSearchText) {
    
  622.     const newSearchIndex = searchResults.indexOf(selectedElementID);
    
  623.     if (newSearchIndex === -1) {
    
  624.       // Only move the selection if the new query
    
  625.       // doesn't match the current selection anymore.
    
  626.       didRequestSearch = true;
    
  627.     } else {
    
  628.       // Selected item still matches the new search query.
    
  629.       // Adjust the index to reflect its position in new results.
    
  630.       searchIndex = newSearchIndex;
    
  631.     }
    
  632.   }
    
  633.   if (didRequestSearch && searchIndex !== null) {
    
  634.     selectedElementID = ((searchResults[searchIndex]: any): number);
    
  635.     selectedElementIndex = store.getIndexOfElementID(
    
  636.       ((selectedElementID: any): number),
    
  637.     );
    
  638.   }
    
  639. 
    
  640.   return {
    
  641.     ...state,
    
  642. 
    
  643.     selectedElementID,
    
  644.     selectedElementIndex,
    
  645. 
    
  646.     searchIndex,
    
  647.     searchResults,
    
  648.     searchText,
    
  649.   };
    
  650. }
    
  651. 
    
  652. function reduceOwnersState(store: Store, state: State, action: Action): State {
    
  653.   let {
    
  654.     numElements,
    
  655.     selectedElementID,
    
  656.     selectedElementIndex,
    
  657.     ownerID,
    
  658.     ownerFlatTree,
    
  659.   } = state;
    
  660.   const {searchIndex, searchResults, searchText} = state;
    
  661. 
    
  662.   let prevSelectedElementIndex = selectedElementIndex;
    
  663. 
    
  664.   switch (action.type) {
    
  665.     case 'HANDLE_STORE_MUTATION':
    
  666.       if (ownerID !== null) {
    
  667.         if (!store.containsElement(ownerID)) {
    
  668.           ownerID = null;
    
  669.           ownerFlatTree = null;
    
  670.           selectedElementID = null;
    
  671.         } else {
    
  672.           ownerFlatTree = store.getOwnersListForElement(ownerID);
    
  673.           if (selectedElementID !== null) {
    
  674.             // Mutation might have caused the index of this ID to shift.
    
  675.             selectedElementIndex = ownerFlatTree.findIndex(
    
  676.               element => element.id === selectedElementID,
    
  677.             );
    
  678.           }
    
  679.         }
    
  680.       } else {
    
  681.         if (selectedElementID !== null) {
    
  682.           // Mutation might have caused the index of this ID to shift.
    
  683.           selectedElementIndex = store.getIndexOfElementID(selectedElementID);
    
  684.         }
    
  685.       }
    
  686.       if (selectedElementIndex === -1) {
    
  687.         // If we couldn't find this ID after mutation, unselect it.
    
  688.         selectedElementIndex = null;
    
  689.         selectedElementID = null;
    
  690.       }
    
  691.       break;
    
  692.     case 'RESET_OWNER_STACK':
    
  693.       ownerID = null;
    
  694.       ownerFlatTree = null;
    
  695.       selectedElementIndex =
    
  696.         selectedElementID !== null
    
  697.           ? store.getIndexOfElementID(selectedElementID)
    
  698.           : null;
    
  699.       break;
    
  700.     case 'SELECT_ELEMENT_AT_INDEX':
    
  701.       if (ownerFlatTree !== null) {
    
  702.         selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
    
  703.       }
    
  704.       break;
    
  705.     case 'SELECT_ELEMENT_BY_ID':
    
  706.       if (ownerFlatTree !== null) {
    
  707.         const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
    
  708.         if (payload === null) {
    
  709.           selectedElementIndex = null;
    
  710.         } else {
    
  711.           selectedElementIndex = ownerFlatTree.findIndex(
    
  712.             element => element.id === payload,
    
  713.           );
    
  714. 
    
  715.           // If the selected element is outside of the current owners list,
    
  716.           // exit the list and select the element in the main tree.
    
  717.           // This supports features like toggling Suspense.
    
  718.           if (selectedElementIndex !== null && selectedElementIndex < 0) {
    
  719.             ownerID = null;
    
  720.             ownerFlatTree = null;
    
  721.             selectedElementIndex = store.getIndexOfElementID(payload);
    
  722.           }
    
  723.         }
    
  724.       }
    
  725.       break;
    
  726.     case 'SELECT_NEXT_ELEMENT_IN_TREE':
    
  727.       if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
    
  728.         if (selectedElementIndex === null) {
    
  729.           selectedElementIndex = 0;
    
  730.         } else if (selectedElementIndex + 1 < ownerFlatTree.length) {
    
  731.           selectedElementIndex++;
    
  732.         }
    
  733.       }
    
  734.       break;
    
  735.     case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
    
  736.       if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
    
  737.         if (selectedElementIndex !== null && selectedElementIndex > 0) {
    
  738.           selectedElementIndex--;
    
  739.         }
    
  740.       }
    
  741.       break;
    
  742.     case 'SELECT_OWNER':
    
  743.       // If the Store doesn't have any owners metadata, don't drill into an empty stack.
    
  744.       // This is a confusing user experience.
    
  745.       if (store.hasOwnerMetadata) {
    
  746.         ownerID = (action: ACTION_SELECT_OWNER).payload;
    
  747.         ownerFlatTree = store.getOwnersListForElement(ownerID);
    
  748. 
    
  749.         // Always force reset selection to be the top of the new owner tree.
    
  750.         selectedElementIndex = 0;
    
  751.         prevSelectedElementIndex = null;
    
  752.       }
    
  753.       break;
    
  754.     default:
    
  755.       // React can bailout of no-op updates.
    
  756.       return state;
    
  757.   }
    
  758. 
    
  759.   // Changes in the selected owner require re-calculating the owners tree.
    
  760.   if (
    
  761.     ownerFlatTree !== state.ownerFlatTree ||
    
  762.     action.type === 'HANDLE_STORE_MUTATION'
    
  763.   ) {
    
  764.     if (ownerFlatTree === null) {
    
  765.       numElements = store.numElements;
    
  766.     } else {
    
  767.       numElements = ownerFlatTree.length;
    
  768.     }
    
  769.   }
    
  770. 
    
  771.   // Keep selected item ID and index in sync.
    
  772.   if (selectedElementIndex !== prevSelectedElementIndex) {
    
  773.     if (selectedElementIndex === null) {
    
  774.       selectedElementID = null;
    
  775.     } else {
    
  776.       if (ownerFlatTree !== null) {
    
  777.         selectedElementID = ownerFlatTree[selectedElementIndex].id;
    
  778.       }
    
  779.     }
    
  780.   }
    
  781. 
    
  782.   return {
    
  783.     ...state,
    
  784. 
    
  785.     numElements,
    
  786.     selectedElementID,
    
  787.     selectedElementIndex,
    
  788. 
    
  789.     searchIndex,
    
  790.     searchResults,
    
  791.     searchText,
    
  792. 
    
  793.     ownerID,
    
  794.     ownerFlatTree,
    
  795.   };
    
  796. }
    
  797. 
    
  798. function reduceSuspenseState(
    
  799.   store: Store,
    
  800.   state: State,
    
  801.   action: Action,
    
  802. ): State {
    
  803.   const {type} = action;
    
  804.   switch (type) {
    
  805.     case 'UPDATE_INSPECTED_ELEMENT_ID':
    
  806.       if (state.inspectedElementID !== state.selectedElementID) {
    
  807.         return {
    
  808.           ...state,
    
  809.           inspectedElementID: state.selectedElementID,
    
  810.         };
    
  811.       }
    
  812.       break;
    
  813.     default:
    
  814.       break;
    
  815.   }
    
  816. 
    
  817.   // React can bailout of no-op updates.
    
  818.   return state;
    
  819. }
    
  820. 
    
  821. type Props = {
    
  822.   children: React$Node,
    
  823. 
    
  824.   // Used for automated testing
    
  825.   defaultInspectedElementID?: ?number,
    
  826.   defaultOwnerID?: ?number,
    
  827.   defaultSelectedElementID?: ?number,
    
  828.   defaultSelectedElementIndex?: ?number,
    
  829. };
    
  830. 
    
  831. // TODO Remove TreeContextController wrapper element once global Context.write API exists.
    
  832. function TreeContextController({
    
  833.   children,
    
  834.   defaultInspectedElementID,
    
  835.   defaultOwnerID,
    
  836.   defaultSelectedElementID,
    
  837.   defaultSelectedElementIndex,
    
  838. }: Props): React.Node {
    
  839.   const bridge = useContext(BridgeContext);
    
  840.   const store = useContext(StoreContext);
    
  841. 
    
  842.   const initialRevision = useMemo(() => store.revision, [store]);
    
  843. 
    
  844.   // This reducer is created inline because it needs access to the Store.
    
  845.   // The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
    
  846.   // so it's okay for the reducer to have an empty dependencies array.
    
  847.   const reducer = useMemo(
    
  848.     () =>
    
  849.       (state: State, action: Action): State => {
    
  850.         const {type} = action;
    
  851.         switch (type) {
    
  852.           case 'GO_TO_NEXT_SEARCH_RESULT':
    
  853.           case 'GO_TO_PREVIOUS_SEARCH_RESULT':
    
  854.           case 'HANDLE_STORE_MUTATION':
    
  855.           case 'RESET_OWNER_STACK':
    
  856.           case 'SELECT_ELEMENT_AT_INDEX':
    
  857.           case 'SELECT_ELEMENT_BY_ID':
    
  858.           case 'SELECT_CHILD_ELEMENT_IN_TREE':
    
  859.           case 'SELECT_NEXT_ELEMENT_IN_TREE':
    
  860.           case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
    
  861.           case 'SELECT_NEXT_SIBLING_IN_TREE':
    
  862.           case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
    
  863.           case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
    
  864.           case 'SELECT_PARENT_ELEMENT_IN_TREE':
    
  865.           case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
    
  866.           case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
    
  867.           case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
    
  868.           case 'SELECT_OWNER':
    
  869.           case 'UPDATE_INSPECTED_ELEMENT_ID':
    
  870.           case 'SET_SEARCH_TEXT':
    
  871.             state = reduceTreeState(store, state, action);
    
  872.             state = reduceSearchState(store, state, action);
    
  873.             state = reduceOwnersState(store, state, action);
    
  874.             state = reduceSuspenseState(store, state, action);
    
  875. 
    
  876.             // If the selected ID is in a collapsed subtree, reset the selected index to null.
    
  877.             // We'll know the correct index after the layout effect will toggle the tree,
    
  878.             // and the store tree is mutated to account for that.
    
  879.             if (
    
  880.               state.selectedElementID !== null &&
    
  881.               store.isInsideCollapsedSubTree(state.selectedElementID)
    
  882.             ) {
    
  883.               return {
    
  884.                 ...state,
    
  885.                 selectedElementIndex: null,
    
  886.               };
    
  887.             }
    
  888. 
    
  889.             return state;
    
  890.           default:
    
  891.             throw new Error(`Unrecognized action "${type}"`);
    
  892.         }
    
  893.       },
    
  894.     [store],
    
  895.   );
    
  896. 
    
  897.   const [state, dispatch] = useReducer(reducer, {
    
  898.     // Tree
    
  899.     numElements: store.numElements,
    
  900.     ownerSubtreeLeafElementID: null,
    
  901.     selectedElementID:
    
  902.       defaultSelectedElementID == null ? null : defaultSelectedElementID,
    
  903.     selectedElementIndex:
    
  904.       defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex,
    
  905. 
    
  906.     // Search
    
  907.     searchIndex: null,
    
  908.     searchResults: [],
    
  909.     searchText: '',
    
  910. 
    
  911.     // Owners
    
  912.     ownerID: defaultOwnerID == null ? null : defaultOwnerID,
    
  913.     ownerFlatTree: null,
    
  914. 
    
  915.     // Inspection element panel
    
  916.     inspectedElementID:
    
  917.       defaultInspectedElementID == null ? null : defaultInspectedElementID,
    
  918.   });
    
  919. 
    
  920.   const dispatchWrapper = useCallback(
    
  921.     (action: Action) => {
    
  922.       dispatch(action);
    
  923.       startTransition(() => {
    
  924.         dispatch({type: 'UPDATE_INSPECTED_ELEMENT_ID'});
    
  925.       });
    
  926.     },
    
  927.     [dispatch],
    
  928.   );
    
  929. 
    
  930.   // Listen for host element selections.
    
  931.   useEffect(() => {
    
  932.     const handleSelectFiber = (id: number) =>
    
  933.       dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
    
  934.     bridge.addListener('selectFiber', handleSelectFiber);
    
  935.     return () => bridge.removeListener('selectFiber', handleSelectFiber);
    
  936.   }, [bridge, dispatchWrapper]);
    
  937. 
    
  938.   // If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
    
  939.   // This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
    
  940.   const prevSelectedElementID = useRef<number | null>(null);
    
  941.   useLayoutEffect(() => {
    
  942.     if (state.selectedElementID !== prevSelectedElementID.current) {
    
  943.       prevSelectedElementID.current = state.selectedElementID;
    
  944. 
    
  945.       if (state.selectedElementID !== null) {
    
  946.         const element = store.getElementByID(state.selectedElementID);
    
  947.         if (element !== null && element.parentID > 0) {
    
  948.           store.toggleIsCollapsed(element.parentID, false);
    
  949.         }
    
  950.       }
    
  951.     }
    
  952.   }, [state.selectedElementID, store]);
    
  953. 
    
  954.   // Mutations to the underlying tree may impact this context (e.g. search results, selection state).
    
  955.   useEffect(() => {
    
  956.     const handleStoreMutated = ([addedElementIDs, removedElementIDs]: [
    
  957.       Array<number>,
    
  958.       Map<number, number>,
    
  959.     ]) => {
    
  960.       dispatchWrapper({
    
  961.         type: 'HANDLE_STORE_MUTATION',
    
  962.         payload: [addedElementIDs, removedElementIDs],
    
  963.       });
    
  964.     };
    
  965. 
    
  966.     // Since this is a passive effect, the tree may have been mutated before our initial subscription.
    
  967.     if (store.revision !== initialRevision) {
    
  968.       // At the moment, we can treat this as a mutation.
    
  969.       // We don't know which Elements were newly added/removed, but that should be okay in this case.
    
  970.       // It would only impact the search state, which is unlikely to exist yet at this point.
    
  971.       dispatchWrapper({
    
  972.         type: 'HANDLE_STORE_MUTATION',
    
  973.         payload: [[], new Map()],
    
  974.       });
    
  975.     }
    
  976. 
    
  977.     store.addListener('mutated', handleStoreMutated);
    
  978. 
    
  979.     return () => store.removeListener('mutated', handleStoreMutated);
    
  980.   }, [dispatchWrapper, initialRevision, store]);
    
  981. 
    
  982.   return (
    
  983.     <TreeStateContext.Provider value={state}>
    
  984.       <TreeDispatcherContext.Provider value={dispatchWrapper}>
    
  985.         {children}
    
  986.       </TreeDispatcherContext.Provider>
    
  987.     </TreeStateContext.Provider>
    
  988.   );
    
  989. }
    
  990. function recursivelySearchTree(
    
  991.   store: Store,
    
  992.   elementID: number,
    
  993.   regExp: RegExp,
    
  994.   searchResults: Array<number>,
    
  995. ): void {
    
  996.   const {children, displayName, hocDisplayNames} = ((store.getElementByID(
    
  997.     elementID,
    
  998.   ): any): Element);
    
  999. 
    
  1000.   if (displayName != null && regExp.test(displayName) === true) {
    
  1001.     searchResults.push(elementID);
    
  1002.   } else if (
    
  1003.     hocDisplayNames != null &&
    
  1004.     hocDisplayNames.length > 0 &&
    
  1005.     hocDisplayNames.some(name => regExp.test(name)) === true
    
  1006.   ) {
    
  1007.     searchResults.push(elementID);
    
  1008.   }
    
  1009. 
    
  1010.   children.forEach(childID =>
    
  1011.     recursivelySearchTree(store, childID, regExp, searchResults),
    
  1012.   );
    
  1013. }
    
  1014. 
    
  1015. function getNearestResultIndex(
    
  1016.   store: Store,
    
  1017.   searchResults: Array<number>,
    
  1018.   selectedElementIndex: number,
    
  1019. ): number {
    
  1020.   const index = searchResults.findIndex(id => {
    
  1021.     const innerIndex = store.getIndexOfElementID(id);
    
  1022.     return innerIndex !== null && innerIndex >= selectedElementIndex;
    
  1023.   });
    
  1024. 
    
  1025.   return index === -1 ? 0 : index;
    
  1026. }
    
  1027. 
    
  1028. export {TreeDispatcherContext, TreeStateContext, TreeContextController};