<commit_before>import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import { DOCS_MODE } from 'global';
import React, { FunctionComponent, MouseEventHandler, ReactNode } from 'react';
import { ControllerStateAndHelpers } from 'downshift';

import { ComponentNode, DocumentNode, Path, RootNode, StoryNode } from './TreeNode';
import { Match, DownshiftItem, isExpandType, SearchResult } from './types';
import { storyLink } from './utils';

const ResultsList = styled.ol({
  listStyle: 'none',
  margin: 0,
  marginLeft: -20,
  marginRight: -20,
  padding: 0,
});

const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted }) => ({
  display: 'block',
  margin: 0,
  padding: 0,
  background: isHighlighted ? `${theme.color.secondary}11` : 'transparent',
  cursor: 'pointer',
}));

const ShowMore = styled.span(({ theme }) => ({
  color: theme.color.mediumdark,
  fontSize: `${theme.typography.size.s1}px`,
}));

const Mark = styled.mark(({ theme }) => ({
  background: 'transparent',
  color: theme.color.secondary,
}));

const PlusIcon = styled(Icons)(({ theme }) => ({
  display: 'inline-block',
  width: 10,
  height: 10,
  marginRight: 5,
  color: theme.color.mediumdark,
}));

const Highlight: FunctionComponent<{ match?: Match }> = React.memo(({ children, match }) => {
  if (!match) return <>{children}</>;
  const { value, indices } = match;
  const { nodes: result } = indices.reduce<{ cursor: number; nodes: ReactNode[] }>(
    ({ cursor, nodes }, [start, end], index, { length }) => {
      /* eslint-disable react/no-array-index-key */
      nodes.push(<span key={`${index}-0`}>{value.slice(cursor, start)}</span>);
      nodes.push(<Mark key={`${index}-1`}>{value.slice(start, end + 1)}</Mark>);
      if (index === length - 1) {
        nodes.push(<span key={`${index}-2`}>{value.slice(end + 1)}</span>);
      }
      /* eslint-enable react/no-array-index-key */
      return { cursor: end + 1, nodes };
    },
    { cursor: 0, nodes: [] }
  );
  return <>{result}</>;
});

const Result: FunctionComponent<
  SearchResult & { icon: string; isHighlighted: boolean; onClick: MouseEventHandler }
> = React.memo(({ item, matches, icon, onClick, ...props }) => {
  const nameMatch = matches.find((match: Match) => match.key === 'name');
  const pathMatches = matches.filter((match: Match) => match.key === 'path');
  const label = (
    <div>
      <strong>
        <Highlight match={nameMatch}>{item.name}</Highlight>
      </strong>
      {item.path && (
        <Path>
          {item.path.map((group, index) => (
            // eslint-disable-next-line react/no-array-index-key
            <span key={index}>
              <Highlight match={pathMatches.find((match: Match) => match.arrayIndex === index)}>
                {group}
              </Highlight>
            </span>
          ))}
        </Path>
      )}
    </div>
  );
  if (DOCS_MODE) {
    const click: MouseEventHandler = (event) => {
      event.preventDefault();
      onClick(event);
    };
    return (
      <ResultRow {...props}>
        <DocumentNode depth={0} onClick={click} href={storyLink(item.id, item.refId)}>
          {label}
        </DocumentNode>
      </ResultRow>
    );
  }
  const TreeNode = item.isComponent ? ComponentNode : StoryNode;
  return (
    <ResultRow {...props}>
      <TreeNode isExpanded={false} depth={0} onClick={onClick}>
        {label}
      </TreeNode>
    </ResultRow>
  );
});

const SearchResults: FunctionComponent<{
  isSearching: boolean;
  results: DownshiftItem[];<commit_msg>Fix callback memoization.<commit_after>import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import { DOCS_MODE } from 'global';
import React, { FunctionComponent, MouseEventHandler, ReactNode, useCallback } from 'react';
import { ControllerStateAndHelpers } from 'downshift';

import { ComponentNode, DocumentNode, Path, RootNode, StoryNode } from './TreeNode';
import { Match, DownshiftItem, isExpandType, SearchResult } from './types';
import { storyLink } from './utils';

const ResultsList = styled.ol({
  listStyle: 'none',
  margin: 0,
  marginLeft: -20,
  marginRight: -20,
  padding: 0,
});

const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted }) => ({
  display: 'block',
  margin: 0,
  padding: 0,
  background: isHighlighted ? `${theme.color.secondary}11` : 'transparent',
  cursor: 'pointer',
}));

const ShowMore = styled.span(({ theme }) => ({
  color: theme.color.mediumdark,
  fontSize: `${theme.typography.size.s1}px`,
}));

const Mark = styled.mark(({ theme }) => ({
  background: 'transparent',
  color: theme.color.secondary,
}));

const PlusIcon = styled(Icons)(({ theme }) => ({
  display: 'inline-block',
  width: 10,
  height: 10,
  marginRight: 5,
  color: theme.color.mediumdark,
}));

const Highlight: FunctionComponent<{ match?: Match }> = React.memo(({ children, match }) => {
  if (!match) return <>{children}</>;
  const { value, indices } = match;
  const { nodes: result } = indices.reduce<{ cursor: number; nodes: ReactNode[] }>(
    ({ cursor, nodes }, [start, end], index, { length }) => {
      /* eslint-disable react/no-array-index-key */
      nodes.push(<span key={`${index}-0`}>{value.slice(cursor, start)}</span>);
      nodes.push(<Mark key={`${index}-1`}>{value.slice(start, end + 1)}</Mark>);
      if (index === length - 1) {
        nodes.push(<span key={`${index}-2`}>{value.slice(end + 1)}</span>);
      }
      /* eslint-enable react/no-array-index-key */
      return { cursor: end + 1, nodes };
    },
    { cursor: 0, nodes: [] }
  );
  return <>{result}</>;
});

const Result: FunctionComponent<
  SearchResult & { icon: string; isHighlighted: boolean; onClick: MouseEventHandler }
> = React.memo(({ item, matches, icon, onClick, ...props }) => {
  const click: MouseEventHandler = useCallback(
    (event) => {
      event.preventDefault();
      onClick(event);
    },
    [onClick]
  );

  const nameMatch = matches.find((match: Match) => match.key === 'name');
  const pathMatches = matches.filter((match: Match) => match.key === 'path');
  const label = (
    <div>
      <strong>
        <Highlight match={nameMatch}>{item.name}</Highlight>
      </strong>
      {item.path && (
        <Path>
          {item.path.map((group, index) => (
            // eslint-disable-next-line react/no-array-index-key
            <span key={index}>
              <Highlight match={pathMatches.find((match: Match) => match.arrayIndex === index)}>
                {group}
              </Highlight>
            </span>
          ))}
        </Path>
      )}
    </div>
  );

  if (DOCS_MODE) {
    return (
      <ResultRow {...props}>
        <DocumentNode depth={0} onClick={click} href={storyLink(item.id, item.refId)}>
          {label}
        </DocumentNode>
      </ResultRow>
    );
  }

  const TreeNode = item.isComponent ? ComponentNode : StoryNode;
  return (
    <ResultRow {...props}>
      <TreeNode isExpanded={false} depth={0} onClick={onClick}>
        {label}
      </TreeNode>
    </ResultRow>
  );
});

const SearchResults: FunctionComponent<{
  isSearching: boolean;
  results: DownshiftItem[];
{'fraction_non_alphanumeric': 0.18274041133963312, 'fraction_numerical': 0.007226236798221234, 'mean_word_length': 3.1102227298686467, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 95, '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': '2266301', 'n_tokens_mistral': 2318, 'n_tokens_neox': 2270, 'n_words': 516}