import React, { useCallback, useMemo, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { ActivityIndicator, Image as RNImage, ScrollView as RNScrollView, Text, View, Platform, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, } from "react-native"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { Fonts } from "@/constants/theme"; import { useSessionStore, type ExplorerFile } from "@/stores/session-store"; import { WebDesktopScrollbarOverlay, useWebDesktopScrollbarMetrics, } from "@/components/web-desktop-scrollbar"; import { highlightCode, darkHighlightColors, lightHighlightColors, type HighlightToken, type HighlightStyle, } from "@getpaseo/highlight"; interface CodeLineProps { tokens: HighlightToken[]; lineNumber: number; gutterWidth: number; colorMap: Record; baseColor: string; } interface FilePreviewBodyProps { preview: ExplorerFile | null; isLoading: boolean; showDesktopWebScrollbar: boolean; isMobile: boolean; filePath: string; } function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function formatFileSize({ size }: { size: number }): string { if (size < 1024) { return `${size} B`; } if (size < 1024 * 1024) { return `${(size / 1024).toFixed(1)} KB`; } return `${(size / (1024 * 1024)).toFixed(1)} MB`; } const CodeLine = React.memo(function CodeLine({ tokens, lineNumber, gutterWidth, colorMap, baseColor, }: CodeLineProps) { return ( {String(lineNumber)} {tokens.map((token, index) => ( {token.text} ))} ); }); const codeLineStyles = StyleSheet.create((theme) => ({ line: { flexDirection: "row", }, gutter: { alignItems: "flex-end", paddingRight: theme.spacing[3], flexShrink: 0, }, gutterText: { fontFamily: Fonts.mono, fontSize: theme.fontSize.sm, lineHeight: theme.fontSize.sm * 1.45, opacity: 0.4, }, lineText: { fontFamily: Fonts.mono, fontSize: theme.fontSize.sm, lineHeight: theme.fontSize.sm * 1.45, flex: 1, }, })); function FilePreviewBody({ preview, isLoading, showDesktopWebScrollbar, isMobile, filePath, }: FilePreviewBodyProps) { const { theme } = useUnistyles(); const isDark = theme.colors.surface0 === "#18181c"; const colorMap = isDark ? darkHighlightColors : lightHighlightColors; const baseColor = isDark ? "#c9d1d9" : "#24292f"; const enablePreviewDesktopScrollbar = showDesktopWebScrollbar; const previewScrollRef = useRef(null); const previewScrollbarMetrics = useWebDesktopScrollbarMetrics(); const highlightedLines = useMemo(() => { if (!preview || preview.kind !== "text") { return null; } return highlightCode(preview.content ?? "", filePath); }, [preview?.kind, preview?.content, filePath]); const gutterWidth = useMemo(() => { if (!highlightedLines) return 0; const digits = String(highlightedLines.length).length; return Math.max(digits * 8 + 12, 28); }, [highlightedLines]); const handlePreviewScroll = useCallback( (event: NativeSyntheticEvent) => { if (enablePreviewDesktopScrollbar) { previewScrollbarMetrics.onScroll(event); } }, [enablePreviewDesktopScrollbar, previewScrollbarMetrics], ); const handlePreviewLayout = useCallback( (event: LayoutChangeEvent) => { if (enablePreviewDesktopScrollbar) { previewScrollbarMetrics.onLayout(event); } }, [enablePreviewDesktopScrollbar, previewScrollbarMetrics], ); if (isLoading && !preview) { return ( Loading file… ); } if (!preview) { return ( No preview available ); } if (preview.kind === "text") { const lines = highlightedLines ?? [[{ text: preview.content ?? "", style: null }]]; const codeLines = ( {lines.map((tokens, index) => ( ))} ); return ( {isMobile ? ( {codeLines} ) : ( {codeLines} )} { previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false }); }} /> ); } if (preview.kind === "image" && preview.content) { return ( { previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false }); }} /> ); } return ( Binary preview unavailable {formatFileSize({ size: preview.size })} ); } export function FilePane({ serverId, workspaceRoot, filePath, }: { serverId: string; workspaceRoot: string; filePath: string; }) { const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile; const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); const normalizedWorkspaceRoot = useMemo(() => workspaceRoot.trim(), [workspaceRoot]); const normalizedFilePath = useMemo(() => trimNonEmpty(filePath), [filePath]); const query = useQuery({ queryKey: ["workspaceFile", serverId, normalizedWorkspaceRoot, normalizedFilePath], enabled: Boolean(client && normalizedWorkspaceRoot && normalizedFilePath), queryFn: async () => { if (!client || !normalizedWorkspaceRoot || !normalizedFilePath) { return { file: null as ExplorerFile | null, error: "Host is not connected" }; } const payload = await client.exploreFileSystem( normalizedWorkspaceRoot, normalizedFilePath, "file", ); return { file: payload.file ?? null, error: payload.error ?? null }; }, staleTime: 5_000, }); return ( {query.data?.error ? ( {query.data.error} ) : null} ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, minHeight: 0, backgroundColor: theme.colors.surface0, }, centerState: { flex: 1, alignItems: "center", justifyContent: "center", padding: theme.spacing[4], }, loadingText: { marginTop: theme.spacing[2], color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, errorText: { color: theme.colors.destructive, fontSize: theme.fontSize.sm, textAlign: "center", }, emptyText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, textAlign: "center", }, binaryMetaText: { marginTop: theme.spacing[2], color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, previewScrollContainer: { flex: 1, minHeight: 0, }, previewContent: { flex: 1, minHeight: 0, }, previewCodeScrollContent: { padding: theme.spacing[4], }, previewImageScrollContent: { flexGrow: 1, padding: theme.spacing[4], alignItems: "center", justifyContent: "center", }, previewImage: { width: "100%", height: 420, }, }));