import * as React from "react"; export interface FileEntry { name: string; kind: "file" | "dir"; } export interface ProjectPathMatch { name: string; path: string; } export interface UsePathCompletionArgs { pathInput: string; setPathInput: (value: string) => void; homeDir: string | null; listDirectoryAbsolute: (path: string) => Promise; debounceMs?: number; } export interface UsePathCompletionResult { matchList: ProjectPathMatch[]; selectedMatch: number; suggestionTail: string; completionError: string | null; currentPartial: string; setSelectedMatch: (index: number) => void; acceptSelectedMatch: () => void; acceptMatch: (path: string) => void; closeSuggestions: () => void; } function isFuzzyMatch(candidate: string, query: string) { if (!query) return true; const lowerCandidate = candidate.toLowerCase(); const lowerQuery = query.toLowerCase(); let idx = 0; for (const char of lowerQuery) { idx = lowerCandidate.indexOf(char, idx); if (idx === -1) return false; idx += 1; } return true; } function getCurrentPartial(input: string) { const trimmed = input.trim(); if (!trimmed) return ""; if (trimmed.endsWith("/")) return ""; const idx = trimmed.lastIndexOf("/"); return idx >= 0 ? trimmed.slice(idx + 1) : trimmed; } export function usePathCompletion({ pathInput, setPathInput, homeDir, listDirectoryAbsolute, debounceMs = 60, }: UsePathCompletionArgs): UsePathCompletionResult { const [matchList, setMatchList] = React.useState([]); const [selectedMatch, setSelectedMatch] = React.useState(0); const [suggestionTail, setSuggestionTail] = React.useState(""); const [completionError, setCompletionError] = React.useState( null, ); React.useEffect(() => { let active = true; async function computeSuggestion() { setCompletionError(null); setSuggestionTail(""); setMatchList([]); setSelectedMatch(0); const trimmed = pathInput.trim(); if (!trimmed) { return; } const endsWithSlash = trimmed.endsWith("/"); let dir = trimmed; let partial = ""; if (!endsWithSlash) { const idx = trimmed.lastIndexOf("/"); if (idx >= 0) { dir = trimmed.slice(0, idx + 1); partial = trimmed.slice(idx + 1); } else { dir = ""; partial = trimmed; } } if (!dir) { if (homeDir) { dir = homeDir.endsWith("/") ? homeDir : `${homeDir}/`; } else { return; } } const dirForListing = dir === "/" ? "/" : dir.replace(/\/+$/, ""); const entries = await listDirectoryAbsolute(dirForListing); if (!active) return; const matches = entries .filter((entry) => entry.kind === "dir") .filter((entry) => isFuzzyMatch(entry.name, partial)) .sort((a, b) => a.name.localeCompare(b.name)) .slice(0, 8); if (matches.length === 0) { return; } const basePrefix = dir.endsWith("/") ? dir : `${dir}/`; const list = matches.map((entry) => ({ name: entry.name, path: `${basePrefix}${entry.name}/`, })); setMatchList(list); } const debounceId = window.setTimeout(() => { computeSuggestion().catch((error) => { console.error(error); if (!active) return; setCompletionError( error instanceof Error ? error.message : "Failed to compute suggestion.", ); }); }, debounceMs); return () => { active = false; window.clearTimeout(debounceId); }; }, [pathInput, homeDir, listDirectoryAbsolute, debounceMs]); React.useEffect(() => { if (matchList.length === 0) { setSuggestionTail(""); return; } const index = Math.min(selectedMatch, matchList.length - 1); const next = matchList[index]; const trimmed = pathInput.trim(); if (next.path.startsWith(trimmed)) { setSuggestionTail(next.path.slice(trimmed.length)); } else { setSuggestionTail(""); } }, [matchList, selectedMatch, pathInput]); const acceptMatch = React.useCallback( (path: string) => { setPathInput(path); }, [setPathInput], ); const acceptSelectedMatch = React.useCallback(() => { const next = matchList[selectedMatch]?.path; if (next) { setPathInput(next); } }, [matchList, selectedMatch, setPathInput]); const closeSuggestions = React.useCallback(() => { setMatchList([]); setSelectedMatch(0); setSuggestionTail(""); setCompletionError(null); }, []); return { matchList, selectedMatch, suggestionTail, completionError, currentPartial: getCurrentPartial(pathInput), setSelectedMatch, acceptSelectedMatch, acceptMatch, closeSuggestions, }; }