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. import type {ReactContext} from 'shared/ReactTypes';
    
  11. 
    
  12. import * as React from 'react';
    
  13. import {createContext, useMemo, useReducer} from 'react';
    
  14. 
    
  15. import type {ReactComponentMeasure, TimelineData, ViewState} from './types';
    
  16. 
    
  17. type State = {
    
  18.   profilerData: TimelineData,
    
  19.   searchIndex: number,
    
  20.   searchRegExp: RegExp | null,
    
  21.   searchResults: Array<ReactComponentMeasure>,
    
  22.   searchText: string,
    
  23. };
    
  24. 
    
  25. type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
    
  26.   type: 'GO_TO_NEXT_SEARCH_RESULT',
    
  27. };
    
  28. type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {
    
  29.   type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
    
  30. };
    
  31. type ACTION_SET_SEARCH_TEXT = {
    
  32.   type: 'SET_SEARCH_TEXT',
    
  33.   payload: string,
    
  34. };
    
  35. 
    
  36. type Action =
    
  37.   | ACTION_GO_TO_NEXT_SEARCH_RESULT
    
  38.   | ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
    
  39.   | ACTION_SET_SEARCH_TEXT;
    
  40. 
    
  41. type Dispatch = (action: Action) => void;
    
  42. 
    
  43. const EMPTY_ARRAY: Array<ReactComponentMeasure> = [];
    
  44. 
    
  45. function reducer(state: State, action: Action): State {
    
  46.   let {searchIndex, searchRegExp, searchResults, searchText} = state;
    
  47. 
    
  48.   switch (action.type) {
    
  49.     case 'GO_TO_NEXT_SEARCH_RESULT':
    
  50.       if (searchResults.length > 0) {
    
  51.         if (searchIndex === -1 || searchIndex + 1 === searchResults.length) {
    
  52.           searchIndex = 0;
    
  53.         } else {
    
  54.           searchIndex++;
    
  55.         }
    
  56.       }
    
  57.       break;
    
  58.     case 'GO_TO_PREVIOUS_SEARCH_RESULT':
    
  59.       if (searchResults.length > 0) {
    
  60.         if (searchIndex === -1 || searchIndex === 0) {
    
  61.           searchIndex = searchResults.length - 1;
    
  62.         } else {
    
  63.           searchIndex--;
    
  64.         }
    
  65.       }
    
  66.       break;
    
  67.     case 'SET_SEARCH_TEXT':
    
  68.       searchText = action.payload;
    
  69.       searchRegExp = null;
    
  70.       searchResults = [];
    
  71. 
    
  72.       if (searchText !== '') {
    
  73.         const safeSearchText = searchText.replace(
    
  74.           /[.*+?^${}()|[\]\\]/g,
    
  75.           '\\$&',
    
  76.         );
    
  77.         searchRegExp = new RegExp(`^${safeSearchText}`, 'i');
    
  78. 
    
  79.         // If something is zoomed in on already, and the new search still contains it,
    
  80.         // don't change the selection (even if overall search results set changes).
    
  81.         let prevSelectedMeasure = null;
    
  82.         if (searchIndex >= 0 && searchResults.length > searchIndex) {
    
  83.           prevSelectedMeasure = searchResults[searchIndex];
    
  84.         }
    
  85. 
    
  86.         const componentMeasures = state.profilerData.componentMeasures;
    
  87. 
    
  88.         let prevSelectedMeasureIndex = -1;
    
  89. 
    
  90.         for (let i = 0; i < componentMeasures.length; i++) {
    
  91.           const componentMeasure = componentMeasures[i];
    
  92.           if (componentMeasure.componentName.match(searchRegExp)) {
    
  93.             searchResults.push(componentMeasure);
    
  94. 
    
  95.             if (componentMeasure === prevSelectedMeasure) {
    
  96.               prevSelectedMeasureIndex = searchResults.length - 1;
    
  97.             }
    
  98.           }
    
  99.         }
    
  100. 
    
  101.         searchIndex =
    
  102.           prevSelectedMeasureIndex >= 0 ? prevSelectedMeasureIndex : 0;
    
  103.       }
    
  104.       break;
    
  105.   }
    
  106. 
    
  107.   return {
    
  108.     profilerData: state.profilerData,
    
  109.     searchIndex,
    
  110.     searchRegExp,
    
  111.     searchResults,
    
  112.     searchText,
    
  113.   };
    
  114. }
    
  115. 
    
  116. export type Context = {
    
  117.   profilerData: TimelineData,
    
  118. 
    
  119.   // Search state
    
  120.   dispatch: Dispatch,
    
  121.   searchIndex: number,
    
  122.   searchRegExp: null,
    
  123.   searchResults: Array<ReactComponentMeasure>,
    
  124.   searchText: string,
    
  125. };
    
  126. 
    
  127. const TimelineSearchContext: ReactContext<Context> = createContext<Context>(
    
  128.   ((null: any): Context),
    
  129. );
    
  130. TimelineSearchContext.displayName = 'TimelineSearchContext';
    
  131. 
    
  132. type Props = {
    
  133.   children: React$Node,
    
  134.   profilerData: TimelineData,
    
  135.   viewState: ViewState,
    
  136. };
    
  137. 
    
  138. function TimelineSearchContextController({
    
  139.   children,
    
  140.   profilerData,
    
  141.   viewState,
    
  142. }: Props): React.Node {
    
  143.   const [state, dispatch] = useReducer<State, State, Action>(reducer, {
    
  144.     profilerData,
    
  145.     searchIndex: -1,
    
  146.     searchRegExp: null,
    
  147.     searchResults: EMPTY_ARRAY,
    
  148.     searchText: '',
    
  149.   });
    
  150. 
    
  151.   const value = useMemo(
    
  152.     () => ({
    
  153.       ...state,
    
  154.       dispatch,
    
  155.     }),
    
  156.     [state],
    
  157.   );
    
  158. 
    
  159.   return (
    
  160.     <TimelineSearchContext.Provider value={value}>
    
  161.       {children}
    
  162.     </TimelineSearchContext.Provider>
    
  163.   );
    
  164. }
    
  165. 
    
  166. export {TimelineSearchContext, TimelineSearchContextController};