import { useEffect, useRef, useCallback, useState } from 'react'; import { findNext, findPrevious, replaceNext, replaceAll, closeSearchPanel, getSearchQuery, setSearchQuery, } from '@codemirror/search'; import type { EditorView } from '@codemirror/view'; import { getActiveView } from '@/lib/editor-commands'; import { useAppStore } from '@/stores/app-store'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { X, ChevronDown, ChevronUp, Replace, ReplaceAll, CaseSensitive, Regex } from 'lucide-react'; export function FindReplaceBar() { const findBarOpen = useAppStore((s) => s.findBarOpen); const toggleFindBar = useAppStore((s) => s.toggleFindBar); const searchRef = useRef(null); const replaceRef = useRef(null); const [caseSensitive, setCaseSensitive] = useState(false); const [useRegex, setUseRegex] = useState(false); const [matchInfo, setMatchInfo] = useState<{ current: number; total: number } | null>(null); const updateMatchCount = useCallback(() => { const view = getActiveView(); if (!view) { setMatchInfo(null); return; } const query = getSearchQuery(view.state); if (!query || !query.search) { setMatchInfo(null); return; } try { const docText = view.state.doc.toString(); const searchStr = query.regexp ? query.search : query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const flags = `g${query.caseSensitive ? '' : 'i'}`; const regex = new RegExp(searchStr, flags); const matches = docText.match(regex); if (!matches) { setMatchInfo({ current: 0, total: 0 }); return; } const selectionHead = view.state.selection.main.head; let currentMatch = 0; const matchPositions: number[] = []; let match; while ((match = regex.exec(docText)) !== null) { matchPositions.push(match.index); if (match.index <= selectionHead && selectionHead <= match.index + match[0].length) { currentMatch = matchPositions.length; } if (matchPositions.length > 10000) break; } setMatchInfo({ current: currentMatch || 1, total: matchPositions.length }); } catch { setMatchInfo(null); } }, []); const executeCommand = useCallback( (fn: (view: EditorView) => boolean | void) => { const view = getActiveView(); if (!view) return false; const result = fn(view); updateMatchCount(); view.focus(); return result; }, [updateMatchCount] ); const handleFindNext = useCallback(() => { executeCommand(findNext); }, [executeCommand]); const handleFindPrev = useCallback(() => { executeCommand(findPrevious); }, [executeCommand]); const handleReplace = useCallback(() => { executeCommand(replaceNext); }, [executeCommand]); const handleReplaceAll = useCallback(() => { executeCommand(replaceAll); }, [executeCommand]); const handleClose = useCallback(() => { const view = getActiveView(); if (view) closeSearchPanel(view); toggleFindBar(); view?.focus(); }, [toggleFindBar]); const handleSearchChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; const view = getActiveView(); if (!view) return; setSearchQuery(view, { search: value, caseSensitive, regexp: useRegex, }); }, [caseSensitive, useRegex] ); const handleReplaceChange = useCallback((e: React.ChangeEvent) => { const view = getActiveView(); if (!view) return; const query = getSearchQuery(view.state); if (query) { setSearchQuery(view, { search: query.search, caseSensitive: query.caseSensitive ?? false, regexp: query.regexp ?? false, replace: e.target.value, }); } }, []); useEffect(() => { if (findBarOpen) { setTimeout(() => searchRef.current?.focus(), 50); } }, [findBarOpen]); useEffect(() => { const handler = () => { if (!useAppStore.getState().findBarOpen) { useAppStore.getState().toggleFindBar(); } }; window.addEventListener('mc:find-toggle', handler); return () => window.removeEventListener('mc:find-toggle', handler); }, []); if (!findBarOpen) return null; return (
{ if (e.key === 'Enter') { e.shiftKey ? handleFindPrev() : handleFindNext(); } if (e.key === 'Escape') handleClose(); if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); replaceRef.current?.focus(); } }} data-testid="find-input" /> {matchInfo && matchInfo.total > 0 && ( {matchInfo.current}/{matchInfo.total} )} { if (e.key === 'Enter') handleReplace(); if (e.key === 'Escape') handleClose(); if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); searchRef.current?.focus(); } }} data-testid="replace-input" />
); }