import { useEffect, useMemo, useRef, useState } from 'react'; import { cn } from '@/lib/utils'; interface Props { query: string; files: string[]; anchorRect: { top: number; left: number }; onSelect: (path: string) => void; onClose: () => void; } function filterAndRank(files: string[], query: string): string[] { const q = query.toLowerCase(); if (!q) { return files.slice(0, 20); } const filenameMatches: string[] = []; const pathOnlyMatches: string[] = []; for (const file of files) { const lower = file.toLowerCase(); if (!lower.includes(q)) continue; const basename = file.split('/').pop() ?? file; if (basename.toLowerCase().includes(q)) { filenameMatches.push(file); } else { pathOnlyMatches.push(file); } } filenameMatches.sort((a, b) => a.localeCompare(b)); pathOnlyMatches.sort((a, b) => a.localeCompare(b)); return [...filenameMatches, ...pathOnlyMatches].slice(0, 20); } export function FileMentionPopover({ query, files, anchorRect, onSelect, onClose, }: Props) { const [highlightIndex, setHighlightIndex] = useState(0); const popoverRef = useRef(null); const filtered = useMemo(() => filterAndRank(files, query), [files, query]); // Reset highlight when query changes useEffect(() => { setHighlightIndex(0); }, [query]); // Keyboard navigation useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === 'ArrowDown') { e.preventDefault(); setHighlightIndex(prev => prev < filtered.length - 1 ? prev + 1 : 0 ); } else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlightIndex(prev => prev > 0 ? prev - 1 : filtered.length - 1 ); } else if (e.key === 'Enter') { e.preventDefault(); if (filtered.length > 0) { onSelect(filtered[highlightIndex] ?? filtered[0]!); } } else if (e.key === 'Escape') { e.preventDefault(); onClose(); } } document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [filtered, highlightIndex, onSelect, onClose]); // Click outside to close useEffect(() => { function handleMouseDown(e: MouseEvent) { if ( popoverRef.current && !popoverRef.current.contains(e.target as Node) ) { onClose(); } } document.addEventListener('mousedown', handleMouseDown); return () => document.removeEventListener('mousedown', handleMouseDown); }, [onClose]); // Scroll highlighted item into view useEffect(() => { const el = popoverRef.current?.querySelector('[data-highlighted="true"]'); if (el) { el.scrollIntoView({ block: 'nearest' }); } }, [highlightIndex]); if (filtered.length === 0) { return (
No matching files
); } return (
{filtered.map((file, i) => ( ))}
); }