vibe-proxy/frontend/src/features/config/hooks/useSourceSearch.ts
2026-08-24 00:10:41 +02:00

133 lines
3.9 KiB
TypeScript

// 源码模式的文档内搜索 —— 从旧 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<ReactCodeMirrorRef | null>(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<typeof useSourceSearch>;