feat: warn about supported or invalid regex

This commit is contained in:
EnixCoda 2024-09-07 18:21:37 +08:00
parent 2c9f74b5f8
commit c3de9ad835
4 changed files with 87 additions and 50 deletions

View file

@ -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<typeof Highlight>['match']) {
function test(
title: string,
text: string,
match: ComponentProps<typeof Highlight>['match'],
expected = text,
) {
it(title, () => {
expect(render(<Highlight text={text} match={match} />).container.textContent).toBe(text)
expect(render(<Highlight text={text} match={match} />).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')

View file

@ -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
}

View file

@ -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 '?<!.'`
: null
: null
return (
<TextInput
ref={ref}
leadingVisual={SearchIcon}
onFocus={e => {
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={
<>
<TextInput.Action
disabled={!value}
onClick={() => onSearch('', searchMode)}
icon={XIcon}
aria-label="Clear"
/>
<TextInput.Action
aria-label={toggleButtonDescription}
sx={{ color: 'fg.subtle' }}
onClick={() => {
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'}
</TextInput.Action>
</>
}
/>
<React.Fragment>
{!!tooltipText && (
<div style={{ position: 'relative' }}>
<Popover open={!!tooltipText} caret="bottom" sx={{ bottom: 0 }}>
<Popover.Content sx={{ padding: 2, color: '#D73A49', width: `var(--gitako-width)` }}>
<AlertIcon aria-label="alert" /> <Text>{tooltipText}</Text>
</Popover.Content>
</Popover>
</div>
)}
<TextInput
ref={ref}
leadingVisual={SearchIcon}
onFocus={e => {
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={
<>
<TextInput.Action
disabled={!value}
onClick={() => onSearch('', searchMode)}
icon={XIcon}
aria-label="Clear"
/>
<TextInput.Action
aria-label={toggleButtonDescription}
sx={{ color: 'fg.subtle' }}
onClick={() => {
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'}
</TextInput.Action>
</>
}
/>
</React.Fragment>
)
}

View file

@ -4,6 +4,8 @@ import { searchKeyToRegexp } from 'utils/general'
import { ModeShape } from '.'
import { Highlight } from '../Highlight'
export const getIsSupportedRegex = (source: string) => !source.match(/\?:|\?=|\?!|\?<=|\?<!/)
export const regexMode: ModeShape = {
getSearchParams(searchKey) {
const regexp = searchKeyToRegexp(searchKey)