import type { ProjectEntry } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { PierreEntryIcon } from "../../components/PierreEntryIcon"; import { cn } from "../../lib/cn"; import { useThemeColor } from "../../lib/useThemeColor"; import { buildFileTree, defaultExpandedTreePaths, flattenFileTree, type VisibleFileTreeNode, } from "./fileTree"; function ancestorPaths(path: string): ReadonlyArray { const parts = path.split("/").filter(Boolean); const ancestors: string[] = []; for (let index = 1; index < parts.length; index += 1) { ancestors.push(parts.slice(0, index).join("/")); } return ancestors; } const FileTreeRow = memo(function FileTreeRow(props: { readonly item: VisibleFileTreeNode; readonly selectedPath: string | null; readonly expanded: boolean; readonly iconColor: string; readonly onPressDirectory: (path: string) => void; readonly onPressFile: (path: string) => void; }) { const { node, depth } = props.item; const selected = node.kind === "file" && node.path === props.selectedPath; return ( { if (node.kind === "directory") { props.onPressDirectory(node.path); return; } props.onPressFile(node.path); }} className={cn( "mx-2 min-h-[42px] flex-row items-center gap-2 rounded-[12px] px-2 active:bg-subtle", selected && "bg-subtle-strong", )} style={{ paddingLeft: 8 + depth * 18 }} > {node.kind === "directory" ? ( ) : ( )} {node.name} {node.kind === "directory" ? ( {node.children.length} ) : null} ); }); export function FileTreeBrowser(props: { readonly entries: ReadonlyArray; readonly error: string | null; readonly isPending: boolean; readonly searchQuery: string; readonly selectedPath: string | null; readonly onRefresh: () => void; readonly onSelectFile: (path: string) => void; }) { const [expandedPaths, setExpandedPaths] = useState>(() => new Set()); const iconColor = String(useThemeColor("--color-icon-muted")); const tree = useMemo(() => buildFileTree(props.entries), [props.entries]); const defaultExpanded = useMemo(() => defaultExpandedTreePaths(tree), [tree]); const visibleNodes = useMemo( () => flattenFileTree({ nodes: tree, expanded: expandedPaths, searchQuery: props.searchQuery, }), [expandedPaths, props.searchQuery, tree], ); useEffect(() => { setExpandedPaths((current) => { if (current.size > 0 || defaultExpanded.size === 0) { return current; } return new Set(defaultExpanded); }); }, [defaultExpanded]); useEffect(() => { if (!props.selectedPath) { return; } setExpandedPaths((current) => { const next = new Set(current); for (const ancestor of ancestorPaths(props.selectedPath ?? "")) { next.add(ancestor); } return next; }); }, [props.selectedPath]); const toggleDirectory = useCallback((path: string) => { setExpandedPaths((current) => { const next = new Set(current); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }, []); return ( {props.error && props.entries.length === 0 ? ( Files unavailable {props.error} ) : ( item.node.path} contentInsetAdjustmentBehavior="automatic" keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" contentContainerStyle={{ paddingVertical: 8 }} refreshControl={ } renderItem={({ item }) => ( )} ListEmptyComponent={ {props.isPending ? ( ) : ( <> No files found {props.searchQuery.trim().length > 0 ? "Try a different search." : "The workspace file index is empty."} )} } /> )} ); }