mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: search with vanilla regexp
This commit is contained in:
parent
27fe8bf8ef
commit
cda88d3956
7 changed files with 58 additions and 79 deletions
|
|
@ -26,7 +26,6 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
static defaultProps: Partial<Props & ConnectorState> = {
|
||||
freeze: false,
|
||||
searchKey: '',
|
||||
searched: false,
|
||||
visibleNodes: null,
|
||||
}
|
||||
|
||||
|
|
@ -126,10 +125,9 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
})
|
||||
|
||||
private renderActions: Node['props']['renderActions'] = node => {
|
||||
const { searchKey, searched, goTo } = this.props
|
||||
const { searchKey, goTo } = this.props
|
||||
return (
|
||||
searchKey &&
|
||||
searched && (
|
||||
searchKey && (
|
||||
<button
|
||||
title={'Reveal in file tree'}
|
||||
className={'go-to-button'}
|
||||
|
|
@ -158,7 +156,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
visibleNodes,
|
||||
freeze,
|
||||
handleKeyDown,
|
||||
handleSearchKeyChange,
|
||||
search,
|
||||
toggleShowSettings,
|
||||
onFocusSearchBar,
|
||||
searchKey,
|
||||
|
|
@ -175,11 +173,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
) : (
|
||||
visibleNodes && (
|
||||
<React.Fragment>
|
||||
<SearchBar
|
||||
searchKey={searchKey}
|
||||
onSearchKeyChange={handleSearchKeyChange}
|
||||
onFocus={onFocusSearchBar}
|
||||
/>
|
||||
<SearchBar searchKey={searchKey} onSearch={search} onFocus={onFocusSearchBar} />
|
||||
{this.renderFiles(visibleNodes)}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,36 @@
|
|||
import * as React from 'react'
|
||||
import cx from 'utils/cx'
|
||||
|
||||
type Props = {
|
||||
onSearchKeyChange: React.FormEventHandler
|
||||
onSearch: (searchKey: string) => void
|
||||
onFocus: React.FocusEventHandler
|
||||
searchKey: string
|
||||
}
|
||||
|
||||
export default function SearchBar({ onSearchKeyChange, onFocus, searchKey }: Props) {
|
||||
export default function SearchBar({ onSearch, onFocus, searchKey }: Props) {
|
||||
return (
|
||||
<div className={'search-input-wrapper'}>
|
||||
<input
|
||||
onFocus={onFocus}
|
||||
tabIndex={0}
|
||||
className="form-control search-input"
|
||||
className={cx('form-control search-input', {
|
||||
error: !isValidRegexpSource(searchKey),
|
||||
})}
|
||||
aria-label="search files"
|
||||
placeholder="Search files (RegEx)"
|
||||
placeholder="Search files (use RegExp)"
|
||||
type="text"
|
||||
onChange={onSearchKeyChange}
|
||||
onChange={({ target: { value } }) => onSearch(value)}
|
||||
value={searchKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function isValidRegexpSource(source: string) {
|
||||
try {
|
||||
new RegExp(source)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,6 +306,10 @@
|
|||
input[type='text'].form-control {
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
|
||||
&.error {
|
||||
border-color: #d73a49;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { GetCreatedMethod, MethodCreator } from 'driver/connect'
|
|||
import * as ini from 'ini'
|
||||
import { Base64 } from 'js-base64'
|
||||
import DOMHelper from 'utils/DOMHelper'
|
||||
import { findNode } from 'utils/general'
|
||||
import { findNode, searchKeyToRegexps } from 'utils/general'
|
||||
import GitHubHelper, { BlobData } from 'utils/GitHubHelper'
|
||||
import treeParser from 'utils/treeParser'
|
||||
import URLHelper from 'utils/URLHelper'
|
||||
|
|
@ -14,12 +14,12 @@ export type ConnectorState = {
|
|||
stateText: string
|
||||
visibleNodes: VisibleNodes | null
|
||||
searchKey: string
|
||||
searched: boolean
|
||||
searched: boolean // derived state from searchKey, = !!searchKey
|
||||
|
||||
init: GetCreatedMethod<typeof init>
|
||||
execAfterRender: GetCreatedMethod<typeof execAfterRender>
|
||||
handleKeyDown: GetCreatedMethod<typeof handleKeyDown>
|
||||
handleSearchKeyChange: GetCreatedMethod<typeof handleSearchKeyChange>
|
||||
search: GetCreatedMethod<typeof search>
|
||||
onNodeClick: GetCreatedMethod<typeof onNodeClick>
|
||||
onFocusSearchBar: GetCreatedMethod<typeof onFocusSearchBar>
|
||||
setUpTree: GetCreatedMethod<typeof setUpTree>
|
||||
|
|
@ -150,7 +150,7 @@ const setUpTree: BoundMethodCreator<
|
|||
compress: compressSingletonFolder,
|
||||
})
|
||||
|
||||
await visibleNodesGenerator.init()
|
||||
visibleNodesGenerator.init()
|
||||
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
dispatch.call(setStateText, '')
|
||||
|
|
@ -278,28 +278,15 @@ const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => eve
|
|||
|
||||
const onFocusSearchBar: BoundMethodCreator = dispatch => () => dispatch.call(focusNode, null, false)
|
||||
|
||||
const handleSearchKeyChange: BoundMethodCreator<
|
||||
[React.FormEvent<HTMLInputElement>]
|
||||
> = dispatch => async event => {
|
||||
const searchKey = event.currentTarget.value
|
||||
await dispatch.call(search, searchKey)
|
||||
}
|
||||
|
||||
const search: BoundMethodCreator<[string]> = dispatch => {
|
||||
let i = 0
|
||||
return async searchKey => {
|
||||
dispatch.set({ searchKey })
|
||||
const j = (i += 1)
|
||||
await visibleNodesGenerator.search(searchKey)
|
||||
if (i === j) {
|
||||
dispatch.set(({ searched }) => ({ searched: !(searched && searchKey === '') }))
|
||||
dispatch.call(updateVisibleNodes)
|
||||
}
|
||||
}
|
||||
const search: BoundMethodCreator<[string]> = dispatch => searchKey => {
|
||||
dispatch.set({ searchKey, searched: searchKey !== '' })
|
||||
const regexps = searchKeyToRegexps(searchKey)
|
||||
visibleNodesGenerator.search(regexps)
|
||||
dispatch.call(updateVisibleNodes)
|
||||
}
|
||||
|
||||
const goTo: BoundMethodCreator<[string[]]> = dispatch => async currentPath => {
|
||||
await visibleNodesGenerator.search('')
|
||||
visibleNodesGenerator.search([])
|
||||
tasksAfterRender.push(() => {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
|
|
@ -360,7 +347,6 @@ export default {
|
|||
handleKeyDown,
|
||||
onFocusSearchBar,
|
||||
search,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
goTo,
|
||||
toggleNodeExpansion,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ export default function cx(...classNames: any[]): string {
|
|||
case 'string':
|
||||
return className
|
||||
case 'object':
|
||||
return cx(
|
||||
...Object.entries(className).map(([key, value]) => (Boolean(value) ? key : null)),
|
||||
)
|
||||
return cx(...Object.entries(className).map(([key, value]) => (value ? key : null)))
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,3 +128,14 @@ export async function JSONRequest(url: string, data: any, method = 'post') {
|
|||
body: JSON.stringify(data),
|
||||
})).json()
|
||||
}
|
||||
|
||||
export function searchKeyToRegexps(searchKey: string) {
|
||||
if (!searchKey) return []
|
||||
|
||||
try {
|
||||
// case-sensitive when searchKey contains uppercase char
|
||||
return [new RegExp(searchKey, /[A-Z]/i.test(searchKey) ? '' : 'i')]
|
||||
} catch (err) {
|
||||
return [/$^/] // matching nothing if failed transforming regexp
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,41 +31,15 @@ export type TreeNode = {
|
|||
accessDenied?: boolean
|
||||
}
|
||||
|
||||
function getFilterFunc(keyRegex: RegExp) {
|
||||
return function filterFunc({ name }: TreeNode) {
|
||||
return keyRegex.test(name)
|
||||
}
|
||||
}
|
||||
|
||||
function filterDuplications<T>(arr: T[]) {
|
||||
return Array.from(new Set(arr))
|
||||
}
|
||||
|
||||
function search(treeNodes: TreeNode[], searchKey: string): TreeNode[] {
|
||||
if (!searchKey) return treeNodes
|
||||
/**
|
||||
* if searchKey is 'abcd'
|
||||
* then keyRegex will be /abcd/i and /a.*?b.*?c.*?d/i
|
||||
*/
|
||||
const regexpGenerators: ((raw: string) => RegExp)[] = [
|
||||
raw => new RegExp(raw, 'i'),
|
||||
raw => new RegExp(raw.split('').join('.*?'), 'i'),
|
||||
]
|
||||
|
||||
const keyRegexes: RegExp[] = []
|
||||
for (const generator of regexpGenerators) {
|
||||
try {
|
||||
const regExp = generator(searchKey)
|
||||
if (keyRegexes.find(keyRegex => keyRegex.source === regExp.source)) continue
|
||||
// prevent duplicated regExp
|
||||
keyRegexes.push(regExp)
|
||||
} catch (err) {
|
||||
// ignore invalid regexp
|
||||
}
|
||||
}
|
||||
function search(treeNodes: TreeNode[], regexps: RegExp[]): TreeNode[] {
|
||||
if (!regexps.length) return treeNodes
|
||||
|
||||
const searchResults = ([] as TreeNode[]).concat(
|
||||
...keyRegexes.map(keyRegex => treeNodes.filter(getFilterFunc(keyRegex))),
|
||||
...regexps.map(keyRegex => treeNodes.filter(({ name }) => keyRegex.test(name))),
|
||||
)
|
||||
return filterDuplications(searchResults)
|
||||
}
|
||||
|
|
@ -121,10 +95,10 @@ class L2 {
|
|||
this.searchedNodes = null
|
||||
}
|
||||
|
||||
search = async (searchKey: string) => {
|
||||
this.compressed = !searchKey
|
||||
this.searchedNodes = searchKey
|
||||
? search(this.l1.nodes, searchKey)
|
||||
search = (regexps: RegExp[]) => {
|
||||
this.compressed = !regexps.length
|
||||
this.searchedNodes = regexps.length
|
||||
? search(this.l1.nodes, regexps)
|
||||
: this.getRoot().contents || []
|
||||
}
|
||||
|
||||
|
|
@ -246,8 +220,8 @@ export default class VisibleNodesGenerator {
|
|||
this.l3 = new L3(this.l1, this.l2)
|
||||
this.l4 = new L4(this.l1, this.l2, this.l3)
|
||||
|
||||
this.search = async (...args) => {
|
||||
const r = await this.l2.search(...args)
|
||||
this.search = (...args) => {
|
||||
const r = this.l2.search(...args)
|
||||
this.l3.generateVisibleNodes()
|
||||
this.l4.focusNode(null)
|
||||
return r
|
||||
|
|
@ -258,8 +232,8 @@ export default class VisibleNodesGenerator {
|
|||
this.focusNode = (...args) => this.l4.focusNode(...args)
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.l2.search('')
|
||||
init() {
|
||||
this.l2.search([])
|
||||
this.l3.generateVisibleNodes()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue