|
| 1 | +import { queryClient } from "@renderer/lib/queryClient"; |
| 2 | +import { trpcReact, trpcVanilla } from "@renderer/trpc/client"; |
| 3 | +import type { MentionItem } from "@shared/types"; |
| 4 | +import { byLengthAsc, Fzf } from "fzf"; |
| 5 | +import { useMemo } from "react"; |
| 6 | + |
| 7 | +export interface FileItem { |
| 8 | + path: string; |
| 9 | + name: string; |
| 10 | + dir: string; |
| 11 | +} |
| 12 | + |
| 13 | +const FILE_DISPLAY_LIMIT = 20; |
| 14 | + |
| 15 | +export function pathToFileItem(path: string): FileItem { |
| 16 | + const parts = path.split("/"); |
| 17 | + const name = parts.pop() ?? path; |
| 18 | + const dir = parts.join("/"); |
| 19 | + return { path, name, dir }; |
| 20 | +} |
| 21 | + |
| 22 | +function transformRawFiles(rawFiles: MentionItem[]): FileItem[] { |
| 23 | + return rawFiles |
| 24 | + .filter((file): file is MentionItem & { path: string } => !!file.path) |
| 25 | + .map((file) => pathToFileItem(file.path)); |
| 26 | +} |
| 27 | + |
| 28 | +function createFzf(files: FileItem[]): Fzf<FileItem[]> { |
| 29 | + return new Fzf(files, { |
| 30 | + selector: (item) => `${item.name} ${item.path}`, |
| 31 | + limit: FILE_DISPLAY_LIMIT, |
| 32 | + tiebreakers: [byLengthAsc], |
| 33 | + }); |
| 34 | +} |
| 35 | + |
| 36 | +export function useRepoFiles(repoPath: string | undefined, enabled = true) { |
| 37 | + const { data: rawFiles, isLoading } = trpcReact.fs.listRepoFiles.useQuery( |
| 38 | + { repoPath: repoPath ?? "" }, |
| 39 | + { enabled: enabled && !!repoPath }, |
| 40 | + ); |
| 41 | + |
| 42 | + const files: FileItem[] = useMemo(() => { |
| 43 | + if (!rawFiles) return []; |
| 44 | + return transformRawFiles(rawFiles); |
| 45 | + }, [rawFiles]); |
| 46 | + |
| 47 | + const fzf = useMemo(() => createFzf(files), [files]); |
| 48 | + |
| 49 | + return { files, fzf, isLoading }; |
| 50 | +} |
| 51 | + |
| 52 | +export function searchFiles( |
| 53 | + fzf: Fzf<FileItem[]>, |
| 54 | + files: FileItem[], |
| 55 | + query: string, |
| 56 | +): FileItem[] { |
| 57 | + if (!query.trim()) { |
| 58 | + return files.slice(0, FILE_DISPLAY_LIMIT); |
| 59 | + } |
| 60 | + const results = fzf.find(query); |
| 61 | + return results.map((result) => result.item); |
| 62 | +} |
| 63 | + |
| 64 | +const fzfCache = new Map< |
| 65 | + string, |
| 66 | + { fzf: Fzf<FileItem[]>; filesLength: number } |
| 67 | +>(); |
| 68 | + |
| 69 | +export async function fetchRepoFiles(repoPath: string): Promise<{ |
| 70 | + files: FileItem[]; |
| 71 | + fzf: Fzf<FileItem[]>; |
| 72 | +}> { |
| 73 | + const queryKey = [ |
| 74 | + ["fs", "listRepoFiles"], |
| 75 | + { input: { repoPath }, type: "query" }, |
| 76 | + ]; |
| 77 | + |
| 78 | + const rawFiles = await queryClient.fetchQuery({ |
| 79 | + queryKey, |
| 80 | + queryFn: () => trpcVanilla.fs.listRepoFiles.query({ repoPath }), |
| 81 | + staleTime: 1000 * 60 * 5, |
| 82 | + }); |
| 83 | + |
| 84 | + const files = transformRawFiles(rawFiles as MentionItem[]); |
| 85 | + |
| 86 | + const cached = fzfCache.get(repoPath); |
| 87 | + if (cached && cached.filesLength === files.length) { |
| 88 | + return { files, fzf: cached.fzf }; |
| 89 | + } |
| 90 | + |
| 91 | + const fzf = createFzf(files); |
| 92 | + fzfCache.set(repoPath, { fzf, filesLength: files.length }); |
| 93 | + return { files, fzf }; |
| 94 | +} |
0 commit comments