<commit_before>import { filter } from 'fuzzaldrin-plus';
import React from 'react';
import { isEqual } from 'lodash';
import { FixtureId, FixtureNamesByPath } from 'react-cosmos-shared2/renderer';
import styled from 'styled-components';
import { createFixtureTree } from '../../shared/fixtureTree';
import {
  KEY_DOWN,
  KEY_ENTER,
  KEY_ESC,
  KEY_TAB,
  KEY_UP
} from '../../shared/keys';
import { FixtureSearchResult } from './FixtureSearchResult';
import { FixtureIdsByPath, flattenFixtureTree } from './flattenFixtureTree';
import { SearchIcon } from '../../shared/icons';

type Props = {
  searchText: string;
  fixturesDir: string;
  fixtureFileSuffix: string;
  fixtures: FixtureNamesByPath;
  selectedFixtureId: null | FixtureId;
  onSetSearchText: (searchText: string) => unknown;
  onClose: () => unknown;
  onSelect: (fixtureId: FixtureId, revealFixture: boolean) => unknown;
};

type ActiveFixturePath = null | string;

export function FixtureSearchOverlay({
  searchText,
  fixturesDir,
  fixtureFileSuffix,
  fixtures,
  selectedFixtureId,
  onSetSearchText,
  onClose,
  onSelect
}: Props) {
  // Flattened fixture IDs are memoized purely to minimize computation
  const fixtureIds = React.useMemo(() => {
    const fixtureTree = createFixtureTree({
      fixtures,
      fixturesDir,
      fixtureFileSuffix
    });
    return flattenFixtureTree(fixtureTree);
  }, [fixtures, fixturesDir, fixtureFileSuffix]);

  const onInputChange = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      onSetSearchText(e.currentTarget.value);
    },
    [onSetSearchText]
  );

  const [matchingFixturePaths, setMatchingFixturePaths] = React.useState(
    getMatchingFixturePaths(fixtureIds, searchText)
  );

  const [activeFixturePath, setActiveFixturePath] = React.useState<
    ActiveFixturePath
  >(() => {
    const selectedFixturePath =
      selectedFixtureId && getFixturePath(fixtureIds, selectedFixtureId);
    return selectedFixturePath || getFirstFixturePath(matchingFixturePaths);
  });

  React.useEffect(() => {
    const newMatchingFixturePaths = getMatchingFixturePaths(
      fixtureIds,
      searchText
    );

    if (!isEqual(newMatchingFixturePaths, matchingFixturePaths)) {
      setMatchingFixturePaths(newMatchingFixturePaths);
      // Reset active fixture to first matching fixture when search changes
      // WARNING: Putting this in a separate effect with only matchingFixturePaths
      // as a dependency looks nicer, but creates a flicker between some renders.
      // On searchText change, the component would first render a new list of
      // matching fixture paths (which may or may not contain the activeFixturePath),
      // and only in the (albeit almost instant) 2nd render would activeFixturePath
      // be updated to equal the first of the new list of matching fixture paths.
      setActiveFixturePath(getFirstFixturePath(newMatchingFixturePaths));
    }
  }, [fixtureIds, matchingFixturePaths, searchText]);

  const onInputKeyDown = React.useMemo(() => {
    function handleEscape() {
      // Clear search on first ESC, close overlay on second
      if (searchText.length > 0) {
        onSetSearchText('');
      } else {
        onClose();
      }
    }

    function handleEnter(revealFixture: boolean) {
      if (activeFixturePath !== null) {
        const fixtureId = fixtureIds[activeFixturePath];
        onSelect(fixtureId, revealFixture);
      }
    }

    function handleUp() {
      if (activeFixturePath) {
        const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
        if (fixtureIndex > 0) {
          setActiveFixturePath(matchingFixturePaths[fixtureIndex - 1]);
        }
      }
    }

    function handleDown() {
      if (activeFixturePath) {
        const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
        if (fixtureIndex < matchingFixturePaths.length - 1) {
          setActiveFixturePath(matchingFixturePaths[fixtureIndex + 1]);
        }
      }
    }

    function handleTab() {
      const matchingFixtureCount = matchingFixturePaths.length;
      if (matchingFixtureCount > 0) {
        if (!activeFixturePath) {
          setActiveFixturePath(matchingFixturePaths[0]);
        } else {
          const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
          const lastMatch = fixtureIndex === matchingFixtureCount - 1;
          if (lastMatch) {
            setActiveFixturePath(matchingFixturePaths[0]);
          } else {
            setActiveFixturePath(matchingFixturePaths[fixtureIndex + 1]);
          }
        }
      }
    }

    function handleTabReverse() {
      const matchingFixtureCount = matchingFixturePaths.length;
      if (matchingFixtureCount > 0) {
        if (!activeFixturePath) {
          setActiveFixturePath(matchingFixturePaths[matchingFixtureCount - 1]);
        } else {
          const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
          const lastMatch = fixtureIndex === 0;
          if (lastMatch) {
            setActiveFixturePath(
              matchingFixturePaths[matchingFixtureCount - 1]
            );
          } else {
            setActiveFixturePath(matchingFixturePaths[fixtureIndex - 1]);
          }
        }
      }
    }

    return (e: React.KeyboardEvent) => {
      switch (e.keyCode) {
        case KEY_ESC:
          e.preventDefault();
          return handleEscape();
        case KEY_ENTER:
          e.preventDefault();
          return handleEnter(e.shiftKey);
        case KEY_UP:
          e.preventDefault();
          return handleUp();
        case KEY_DOWN:
          e.preventDefault();
          return handleDown();
        case KEY_TAB:
          e.preventDefault();
          return e.shiftKey ? handleTabReverse() : handleTab();
        default:
        // Nada
      }
    };
  }, [
    searchText.length,
    onSetSearchText,
    onClose,
    activeFixturePath,
    fixtureIds,
    onSelect,
    matchingFixturePaths
  ]);

  const inputRef = React.useRef<HTMLInputElement>(null);

  // Auto focus when search input is created
  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);
<commit_msg>Select previous search text when opening fixture search (#1043)<commit_after>import { filter } from 'fuzzaldrin-plus';
import React from 'react';
import { isEqual } from 'lodash';
import { FixtureId, FixtureNamesByPath } from 'react-cosmos-shared2/renderer';
import styled from 'styled-components';
import { createFixtureTree } from '../../shared/fixtureTree';
import {
  KEY_DOWN,
  KEY_ENTER,
  KEY_ESC,
  KEY_TAB,
  KEY_UP
} from '../../shared/keys';
import { FixtureSearchResult } from './FixtureSearchResult';
import { FixtureIdsByPath, flattenFixtureTree } from './flattenFixtureTree';
import { SearchIcon } from '../../shared/icons';

type Props = {
  searchText: string;
  fixturesDir: string;
  fixtureFileSuffix: string;
  fixtures: FixtureNamesByPath;
  selectedFixtureId: null | FixtureId;
  onSetSearchText: (searchText: string) => unknown;
  onClose: () => unknown;
  onSelect: (fixtureId: FixtureId, revealFixture: boolean) => unknown;
};

type ActiveFixturePath = null | string;

export function FixtureSearchOverlay({
  searchText,
  fixturesDir,
  fixtureFileSuffix,
  fixtures,
  selectedFixtureId,
  onSetSearchText,
  onClose,
  onSelect
}: Props) {
  // Flattened fixture IDs are memoized purely to minimize computation
  const fixtureIds = React.useMemo(() => {
    const fixtureTree = createFixtureTree({
      fixtures,
      fixturesDir,
      fixtureFileSuffix
    });
    return flattenFixtureTree(fixtureTree);
  }, [fixtures, fixturesDir, fixtureFileSuffix]);

  const onInputChange = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      onSetSearchText(e.currentTarget.value);
    },
    [onSetSearchText]
  );

  const [matchingFixturePaths, setMatchingFixturePaths] = React.useState(
    getMatchingFixturePaths(fixtureIds, searchText)
  );

  const [activeFixturePath, setActiveFixturePath] = React.useState<
    ActiveFixturePath
  >(() => {
    const selectedFixturePath =
      selectedFixtureId && getFixturePath(fixtureIds, selectedFixtureId);
    return selectedFixturePath || getFirstFixturePath(matchingFixturePaths);
  });

  React.useEffect(() => {
    const newMatchingFixturePaths = getMatchingFixturePaths(
      fixtureIds,
      searchText
    );

    if (!isEqual(newMatchingFixturePaths, matchingFixturePaths)) {
      setMatchingFixturePaths(newMatchingFixturePaths);
      // Reset active fixture to first matching fixture when search changes
      // WARNING: Putting this in a separate effect with only matchingFixturePaths
      // as a dependency looks nicer, but creates a flicker between some renders.
      // On searchText change, the component would first render a new list of
      // matching fixture paths (which may or may not contain the activeFixturePath),
      // and only in the (albeit almost instant) 2nd render would activeFixturePath
      // be updated to equal the first of the new list of matching fixture paths.
      setActiveFixturePath(getFirstFixturePath(newMatchingFixturePaths));
    }
  }, [fixtureIds, matchingFixturePaths, searchText]);

  const onInputKeyDown = React.useMemo(() => {
    function handleEscape() {
      onClose();
    }

    function handleEnter(revealFixture: boolean) {
      if (activeFixturePath !== null) {
        const fixtureId = fixtureIds[activeFixturePath];
        onSelect(fixtureId, revealFixture);
      }
    }

    function handleUp() {
      if (activeFixturePath) {
        const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
        if (fixtureIndex > 0) {
          setActiveFixturePath(matchingFixturePaths[fixtureIndex - 1]);
        }
      }
    }

    function handleDown() {
      if (activeFixturePath) {
        const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
        if (fixtureIndex < matchingFixturePaths.length - 1) {
          setActiveFixturePath(matchingFixturePaths[fixtureIndex + 1]);
        }
      }
    }

    function handleTab() {
      const matchingFixtureCount = matchingFixturePaths.length;
      if (matchingFixtureCount > 0) {
        if (!activeFixturePath) {
          setActiveFixturePath(matchingFixturePaths[0]);
        } else {
          const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
          const lastMatch = fixtureIndex === matchingFixtureCount - 1;
          if (lastMatch) {
            setActiveFixturePath(matchingFixturePaths[0]);
          } else {
            setActiveFixturePath(matchingFixturePaths[fixtureIndex + 1]);
          }
        }
      }
    }

    function handleTabReverse() {
      const matchingFixtureCount = matchingFixturePaths.length;
      if (matchingFixtureCount > 0) {
        if (!activeFixturePath) {
          setActiveFixturePath(matchingFixturePaths[matchingFixtureCount - 1]);
        } else {
          const fixtureIndex = matchingFixturePaths.indexOf(activeFixturePath);
          const lastMatch = fixtureIndex === 0;
          if (lastMatch) {
            setActiveFixturePath(
              matchingFixturePaths[matchingFixtureCount - 1]
            );
          } else {
            setActiveFixturePath(matchingFixturePaths[fixtureIndex - 1]);
          }
        }
      }
    }

    return (e: React.KeyboardEvent) => {
      switch (e.keyCode) {
        case KEY_ESC:
          e.preventDefault();
          return handleEscape();
        case KEY_ENTER:
          e.preventDefault();
          return handleEnter(e.shiftKey);
        case KEY_UP:
          e.preventDefault();
          return handleUp();
        case KEY_DOWN:
          e.preventDefault();
          return handleDown();
        case KEY_TAB:
          e.preventDefault();
          return e.shiftKey ? handleTabReverse() : handleTab();
        default:
        // Nada
      }
    };
  }, [onClose, activeFixturePath, fixtureIds, onSelect, matchingFixturePaths]);

  const inputRef = React.useRef<HTMLInputElement>(null);

  // Auto focus when search input is created
  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
      inputRef.current.select();
    }
  }, []);

{'fraction_non_alphanumeric': 0.09723352318958503, 'fraction_numerical': 0.003010577705451587, 'mean_word_length': 2.93437900128041, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 36, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5455572', 'n_tokens_mistral': 3550, 'n_tokens_neox': 3477, 'n_words': 783}