// 源码模式的文档内搜索 —— 从旧 pages/ConfigPage.tsx 逐字提取。 // 手写 indexOf 扫描 + 光标定位(大小写不敏感、回绕),不依赖 @codemirror/search 面板。 import { useCallback, useRef, useState } from 'react'; import type { ReactCodeMirrorRef } from '@uiw/react-codemirror'; export function useSourceSearch() { const editorRef = useRef(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState<{ current: number; total: number }>({ current: 0, total: 0, }); const [lastSearchedQuery, setLastSearchedQuery] = useState(''); const performSearch = useCallback((query: string, direction: 'next' | 'prev' = 'next') => { if (!query || !editorRef.current?.view) return; const view = editorRef.current.view; const doc = view.state.doc.toString(); const matches: number[] = []; const lowerQuery = query.toLowerCase(); const lowerDoc = doc.toLowerCase(); let pos = 0; while (pos < lowerDoc.length) { const index = lowerDoc.indexOf(lowerQuery, pos); if (index === -1) break; matches.push(index); pos = index + 1; } if (matches.length === 0) { setSearchResults({ current: 0, total: 0 }); return; } // Find current match based on cursor position const selection = view.state.selection.main; const cursorPos = direction === 'prev' ? selection.from : selection.to; let currentIndex = 0; if (direction === 'next') { // Find next match after cursor for (let i = 0; i < matches.length; i++) { if (matches[i] > cursorPos) { currentIndex = i; break; } // If no match after cursor, wrap to first if (i === matches.length - 1) { currentIndex = 0; } } } else { // Find previous match before cursor for (let i = matches.length - 1; i >= 0; i--) { if (matches[i] < cursorPos) { currentIndex = i; break; } // If no match before cursor, wrap to last if (i === 0) { currentIndex = matches.length - 1; } } } const matchPos = matches[currentIndex]; setSearchResults({ current: currentIndex + 1, total: matches.length }); // Scroll to and select the match view.dispatch({ selection: { anchor: matchPos, head: matchPos + query.length }, scrollIntoView: true, }); view.focus(); }, []); const handleSearchChange = useCallback((value: string) => { setSearchQuery(value); // Do not auto-search on each keystroke. Clear previous results when query changes. if (!value) { setSearchResults({ current: 0, total: 0 }); setLastSearchedQuery(''); } else { setSearchResults({ current: 0, total: 0 }); } }, []); const executeSearch = useCallback( (direction: 'next' | 'prev' = 'next') => { if (!searchQuery) return; setLastSearchedQuery(searchQuery); performSearch(searchQuery, direction); }, [searchQuery, performSearch] ); const handleSearchKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); executeSearch(e.shiftKey ? 'prev' : 'next'); } }, [executeSearch] ); const handlePrevMatch = useCallback(() => { if (!lastSearchedQuery) return; performSearch(lastSearchedQuery, 'prev'); }, [lastSearchedQuery, performSearch]); const handleNextMatch = useCallback(() => { if (!lastSearchedQuery) return; performSearch(lastSearchedQuery, 'next'); }, [lastSearchedQuery, performSearch]); return { editorRef, searchQuery, searchResults, lastSearchedQuery, handleSearchChange, executeSearch, handleSearchKeyDown, handlePrevMatch, handleNextMatch, }; } export type UseSourceSearchResult = ReturnType;