mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: warn about supported or invalid regex
This commit is contained in:
parent
2c9f74b5f8
commit
c3de9ad835
4 changed files with 87 additions and 50 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue