import React, { FC, FormEventHandler, memo, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' import { useLayoutContext } from '@/shared/context/layout-context' import { useProjectContext } from '@/shared/context/project-context' import { MatchedFile as MatchedFileType, searchSnapshot, } from '../util/search-snapshot' import { SearchQuery } from '@codemirror/search' import { debugConsole } from '@/utils/debugging' import useEventListener from '@/shared/hooks/use-event-listener' import { Col, Form, Row } from 'react-bootstrap' import OLFormControl from '@/features/ui/components/ol/ol-form-control' import Button from '@/features/ui/components/bootstrap-5/button' import Notification from '@/shared/components/notification' import '../../stylesheets/full-project-search.scss' import { userStyles } from '@/shared/utils/styles' import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { FullProjectMatchCounts } from './full-project-match-counts' import { FullProjectSearchModifiers } from './full-project-search-modifiers' import { isMac } from '@/shared/utils/os' import { PanelHeading } from '@/shared/components/panel-heading' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import { createRegExp } from '../util/regexp' import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' import { FullProjectSearchResults } from './full-project-search-results' import { signalWithTimeout } from '@/utils/abort-signal' import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' import RailPanelHeader from '@/features/ide-redesign/components/rail-panel-header' import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' const FullProjectSearchUI: FC = () => { const { t } = useTranslation() const { setProjectSearchIsOpen } = useLayoutContext() const { projectSnapshot } = useProjectContext() const { openDocs } = useEditorManagerContext() const { pathInFolder } = useFileTreePathContext() const newEditor = useIsNewEditorEnabled() const { currentDocument: currentDoc } = useEditorOpenDocContext() const [loading, setLoading] = useState(false) const [error, setError] = useState() const [matchedFiles, setMatchedFiles] = useState() const { userSettings } = useUserSettingsContext() const { fontFamily, fontSize } = useMemo( () => userStyles(userSettings), [userSettings] ) const activeOverallTheme = useActiveOverallTheme() const abortControllerRef = useRef(null) // start fetching the snapshot when the project search UI opens useEffect(() => { projectSnapshot.refresh().catch(error => { debugConsole.error(error) }) }, [projectSnapshot]) const currentDocPath = useMemo(() => { return currentDoc && pathInFolder(currentDoc.doc_id) }, [currentDoc, pathInFolder]) const handleSubmit: React.FormEventHandler = useCallback( async event => { event.preventDefault() setMatchedFiles(undefined) abortControllerRef.current?.abort() abortControllerRef.current = new AbortController() const data = new FormData(event.target as HTMLFormElement) const searchQuery = new SearchQuery({ search: data.get('search') as string, // replace: data.get('replace') as string, caseSensitive: data.get('caseSensitive') === 'on', regexp: data.get('regexp') === 'on', wholeWord: data.get('wholeWord') === 'on', literal: data.get('regexp') !== 'on', }) if (searchQuery.regexp) { try { createRegExp(searchQuery) } catch (error) { setError(t('invalid_regular_expression')) return } } setLoading(true) setError(undefined) try { await openDocs.awaitBufferedOps( signalWithTimeout(abortControllerRef.current.signal, 5000) ) await projectSnapshot.refresh() if (!abortControllerRef.current.signal.aborted) { const results = await searchSnapshot(projectSnapshot, searchQuery) setMatchedFiles(results) } } catch (error) { debugConsole.error(error) setError(t('generic_something_went_wrong')) } finally { setLoading(false) } }, [openDocs, projectSnapshot, t] ) const searchInputRef = useRef(null) const handleKeyDown: React.KeyboardEventHandler = useCallback( event => { if (event.key === 'Escape') { setProjectSearchIsOpen(false) } }, [setProjectSearchIsOpen] ) useEventListener( 'keydown', useCallback((event: KeyboardEvent) => { if ( (isMac ? event.metaKey : event.ctrlKey) && event.shiftKey && event.code === 'KeyF' ) { searchInputRef.current?.focus() } }, []) ) const modifiersRef = useRef<{ setQuery(query: SearchQuery): void }>(null) useEventListener( 'editor:full-project-search', useCallback((event: CustomEvent) => { if (modifiersRef.current != null) { modifiersRef.current.setQuery(event.detail) } if (searchInputRef.current != null) { searchInputRef.current.value = event.detail.search searchInputRef.current.form?.dispatchEvent( new Event('submit', { cancelable: true, bubbles: true }) ) } }, []) ) // clear the results when the form is cleared const handleInput: FormEventHandler = useCallback(event => { if ( event instanceof InputEvent && event.inputType === undefined && (event.target as HTMLInputElement).value.length === 0 ) { setMatchedFiles(undefined) } }, []) const variableStyle = { '--font-family': fontFamily, '--font-size': fontSize, } as React.CSSProperties return (
{newEditor ? ( ) : ( setProjectSearchIsOpen(false)} splitTestName="full-project-search" /> )}
{error && }
{matchedFiles && ( )}
) } export default memo(FullProjectSearchUI)