({ onDone, onInsert }: Props)
| 26 | * Fuzzy file finder with a syntax-highlighted preview of the focused file. |
| 27 | */ |
| 28 | export function QuickOpenDialog({ onDone, onInsert }: Props): React.ReactNode { |
| 29 | useRegisterOverlay('quick-open'); |
| 30 | const { columns, rows } = useTerminalSize(); |
| 31 | // Chrome (title + search + hints + pane border + gaps) eats ~14 rows. |
| 32 | // Shrink the list on short terminals so the dialog doesn't clip. |
| 33 | const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14)); |
| 34 | |
| 35 | const [results, setResults] = useState<string[]>([]); |
| 36 | const [query, setQuery] = useState(''); |
| 37 | const [focusedPath, setFocusedPath] = useState<string | undefined>(undefined); |
| 38 | const [preview, setPreview] = useState<{ |
| 39 | path: string; |
| 40 | content: string; |
| 41 | } | null>(null); |
| 42 | const queryGenRef = useRef(0); |
| 43 | useEffect(() => () => void queryGenRef.current++, []); |
| 44 | |
| 45 | const previewOnRight = columns >= 120; |
| 46 | // Side preview sits in a fixed-height row alongside the list (visibleCount |
| 47 | // rows), so overflowing that height garbles the layout — cap to fit, minus |
| 48 | // one for the path header line. |
| 49 | const effectivePreviewLines = previewOnRight ? VISIBLE_RESULTS - 1 : PREVIEW_LINES; |
| 50 | |
| 51 | // A generation counter invalidates stale results if the user types faster |
| 52 | // than the index can respond. |
| 53 | const handleQueryChange = (q: string) => { |
| 54 | setQuery(q); |
| 55 | const gen = ++queryGenRef.current; |
| 56 | if (!q.trim()) { |
| 57 | // generateFileSuggestions('') returns raw readdir() of cwd (designed for |
| 58 | // @-mentions). For Quick Open that's just noise — show the empty state. |
| 59 | setResults([]); |
| 60 | return; |
| 61 | } |
| 62 | void generateFileSuggestions(q, true).then(items => { |
| 63 | if (gen !== queryGenRef.current) return; |
| 64 | // Filter out directory entries — they come back with a trailing path.sep |
| 65 | // from getTopLevelPaths() and would cause readFileInRange to throw EISDIR, |
| 66 | // leaving the preview pane stuck on "Loading preview…". |
| 67 | // Normalize separators to '/' so truncatePathMiddle (which uses |
| 68 | // lastIndexOf('/')) can find the filename on Windows too. |
| 69 | const paths = items |
| 70 | .filter(i => i.id.startsWith('file-')) |
| 71 | .map(i => i.displayText) |
| 72 | .filter(p => !p.endsWith(path.sep)) |
| 73 | .map(p => p.split(path.sep).join('/')); |
| 74 | setResults(paths); |
| 75 | }); |
| 76 | }; |
| 77 | |
| 78 | // Load a short preview of the focused file. Each navigation aborts the |
| 79 | // previous read so holding ↓ doesn't pile up whole-file reads and so a |
| 80 | // slow early read can't overwrite a faster later one. The stale preview |
| 81 | // stays visible until the new one arrives — renderPreview overlays a dim |
| 82 | // loading indicator rather than blanking the pane. |
| 83 | useEffect(() => { |
| 84 | if (!focusedPath) { |
| 85 | // No results — clear so the empty-state renders instead of a stale |
nothing calls this directly
no test coverage detected