From c3de9ad835b87ade7ea07dd4b42eff0e64f976c0 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 7 Sep 2024 18:21:37 +0800 Subject: [PATCH] feat: warn about supported or invalid regex --- src/components/Highlight.test.tsx | 20 ++-- src/components/Highlight.tsx | 4 +- src/components/SearchBar.tsx | 111 ++++++++++++++--------- src/components/searchModes/regexMode.tsx | 2 + 4 files changed, 87 insertions(+), 50 deletions(-) diff --git a/src/components/Highlight.test.tsx b/src/components/Highlight.test.tsx index 5d1fbc3..56fe92b 100644 --- a/src/components/Highlight.test.tsx +++ b/src/components/Highlight.test.tsx @@ -2,14 +2,20 @@ import { render } from '@testing-library/react' import React, { ComponentProps } from 'react' import { Highlight } from './Highlight' -function test(title: string, text: string, match?: ComponentProps['match']) { +function test( + title: string, + text: string, + match: ComponentProps['match'], + expected = text, +) { it(title, () => { - expect(render().container.textContent).toBe(text) + expect(render().container.textContent).toBe(expected) }) } -test('sample', 'abc', undefined) -test('sample', 'abc', /./) -test('sample', 'abc', /../) -test('sample', 'abc', /.../) -test('sample', 'abc', /..../) +test('abc', 'abc', undefined) +test('abc1', 'abc', /./) +test('abc2', 'abc', /../) +test('abc3', 'abc', /.../) +test('abc4', 'abc', /..../) +test('abcd', 'abcd', /^((?!ab)cd)*$/, 'abcd') diff --git a/src/components/Highlight.tsx b/src/components/Highlight.tsx index 4d73e58..243994e 100644 --- a/src/components/Highlight.tsx +++ b/src/components/Highlight.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { getIsSupportedRegex } from './searchModes/regexMode' export const Highlight = function Highlight({ text, match }: { text: string; match?: RegExp }) { const $match = React.useMemo( @@ -20,7 +21,8 @@ type ElementMeta = [tag: string, content: string] function getChunks(text: string, match: RegExp | null): ElementMeta[] { const contents: ElementMeta[] = [] - if (match === null) { + const isSupportedMode = match && getIsSupportedRegex(match.source) + if (!isSupportedMode) { contents.push(['span', text]) return contents } diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 8f44017..0ca6202 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,10 +1,11 @@ -import { SearchIcon, XIcon } from '@primer/octicons-react' -import { TextInput, TextInputProps } from '@primer/react' +import { AlertIcon, SearchIcon, XIcon } from '@primer/octicons-react' +import { Popover, Text, TextInput, TextInputProps } from '@primer/react' import { useConfigs } from 'containers/ConfigsContext' import * as React from 'react' import { formatWithShortcut, isValidRegexpSource } from 'utils/general' import { useFocusOnPendingTarget } from './FocusTarget' import { SearchMode } from './searchModes' +import { getIsSupportedRegex } from './searchModes/regexMode' type Props = { value: string @@ -26,49 +27,75 @@ export function SearchBar({ onSearch, onFocus, value }: Props) { ? 'Match file name with regular expression.' : `Match file path sequence with plain input.` - const validationStatus = React.useMemo( - () => (searchMode === 'regex' && !isValidRegexpSource(value) ? 'error' : undefined), + const isInputValid = React.useMemo( + () => + ({ + regex: isValidRegexpSource(value), + fuzzy: true, + }[searchMode]), + [value, searchMode], + ) + const isSupportedRegex = React.useMemo( + () => !(searchMode === 'regex' && !getIsSupportedRegex(value)), [value, searchMode], ) + const tooltipText = value + ? !isInputValid + ? 'Invalid regular expression.' + : !isSupportedRegex + ? `Highlight is not supported for regular expression containing '?:', '?=', '?!', '?<=', or '? { - onFocus(e) - e.target.select() - }} - block - sx={{ borderRadius: 0 }} - className={'search-input'} - aria-label="Search files" - placeholder={formatWithShortcut(`Search files`, focusSearchInputShortcut)} - onChange={({ target: { value } }) => onSearch(value, searchMode)} - value={value} - validationStatus={validationStatus} - trailingAction={ - <> - onSearch('', searchMode)} - icon={XIcon} - aria-label="Clear" - /> - { - const newMode = searchMode === 'regex' ? 'fuzzy' : 'regex' - configs.onChange({ searchMode: newMode }) - // Skip search if no input to prevent resetting folder expansions - if (value) onSearch(value, newMode) - }} - > - {searchMode === 'regex' ? '.*$' : 'a/b'} - - - } - /> + + {!!tooltipText && ( +
+ + + {tooltipText} + + +
+ )} + { + onFocus(e) + e.target.select() + }} + block + sx={{ borderRadius: 0 }} + className={'search-input'} + aria-label="Search files" + placeholder={formatWithShortcut(`Search files`, focusSearchInputShortcut)} + onChange={({ target: { value } }) => onSearch(value, searchMode)} + value={value} + trailingAction={ + <> + onSearch('', searchMode)} + icon={XIcon} + aria-label="Clear" + /> + { + const newMode = searchMode === 'regex' ? 'fuzzy' : 'regex' + configs.onChange({ searchMode: newMode }) + // Skip search if no input to prevent resetting folder expansions + if (value) onSearch(value, newMode) + }} + > + {searchMode === 'regex' ? '.*$' : 'a/b'} + + + } + /> +
) } diff --git a/src/components/searchModes/regexMode.tsx b/src/components/searchModes/regexMode.tsx index 95cd473..b5805a0 100644 --- a/src/components/searchModes/regexMode.tsx +++ b/src/components/searchModes/regexMode.tsx @@ -4,6 +4,8 @@ import { searchKeyToRegexp } from 'utils/general' import { ModeShape } from '.' import { Highlight } from '../Highlight' +export const getIsSupportedRegex = (source: string) => !source.match(/\?:|\?=|\?!|\?<=|\?