/** * Quick Open dialog (Cmd+P) — fuzzy file search using cmdk. * * Escape closes dialog (not the editor overlay). * Loads ALL project files via backend API on mount (not limited to expanded dirs). */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useStore } from '@renderer/store'; import { Command } from 'cmdk'; import { Loader2 } from 'lucide-react'; import { getFileIcon } from './fileIcons'; import type { QuickOpenFile } from '@shared/types/editor'; // ============================================================================= // Types // ============================================================================= interface QuickOpenDialogProps { onClose: () => void; onSelectFile: (filePath: string) => void; } // ============================================================================= // Component // ============================================================================= export const QuickOpenDialog = ({ onClose, onSelectFile, }: QuickOpenDialogProps): React.ReactElement => { const projectPath = useStore((s) => s.editorProjectPath); const dialogRef = useRef(null); const [allFiles, setAllFiles] = useState([]); const [loading, setLoading] = useState(true); // Load all project files on mount via backend API useEffect(() => { let cancelled = false; // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change setLoading(true); window.electronAPI.editor .listFiles() .then((files) => { if (!cancelled) { setAllFiles(files); setLoading(false); } }) .catch(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [projectPath]); // Escape to close dialog (not overlay) const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); } }, [onClose] ); useEffect(() => { window.addEventListener('keydown', handleKeyDown, true); return () => window.removeEventListener('keydown', handleKeyDown, true); }, [handleKeyDown]); const handleSelect = useCallback( (value: string) => { // value is relativePath from cmdk — look up full path const file = allFiles.find((f) => f.relativePath === value); if (file) { onSelectFile(file.path); onClose(); } }, [allFiles, onSelectFile, onClose] ); // Memoize file icon lookups const fileItems = useMemo( () => allFiles.map((file) => ({ ...file, iconInfo: getFileIcon(file.name), })), [allFiles] ); return (
{/* Backdrop */}
{ if (e.key === 'Escape') onClose(); }} role="presentation" /> {/* Dialog */}
{loading && (
Loading files...
)} {!loading && ( No files found )} {fileItems.map((file) => { const Icon = file.iconInfo.icon; return ( handleSelect(file.relativePath)} className="flex cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-sm text-text-secondary aria-selected:bg-surface-raised aria-selected:text-text" > {file.name} {file.relativePath} ); })}
); };